The Future Demand Client API uses AWS Cognito as its identity provider. Every authenticated request carries a Bearer access token plus a header that tells the backend which partner (tenant) you’re acting under.
The OpenAPI spec lists a security scheme called apikey, but its description says Copy your Cognito JWT Token below: 'Bearer <JWT>'. There is no separate static API key — you obtain a JWT the same way the reference webapp does, via POST /auth. Treat any “API key” mention in the spec as “Bearer JWT”.

The two headers you always send

Authorization: Bearer <access_token>
X-Preferred-Partner-Id: <partner_id>
HeaderRequired?What it is
AuthorizationAlwaysCognito AccessToken returned from /auth. Refresh ~5 min before ExpiresIn.
X-Preferred-Partner-IdAlways when the user belongs to >1 partner; safest to always sendThe partner the request applies to. Must be one of the user’s cognito:groups matching `/(partner-id-.+fd\d+)/`. The backend scopes all reads/writes by this.
The reference webapp attaches both via an axios transformRequest (see webapp/src/api/_init.js). Mirror this in your client.

1. Sign in

curl -s -X POST "https://client-api.prd.future-demand.com/api/v3/auth" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "user@partner.com",
    "password": "hunter2"
  }'
The response is one of three shapes — branch on the presence of AccessToken and the value of Challenge:
{
  "AccessToken":   "eyJraWQ...",
  "IdToken":       "eyJraWQ...",
  "RefreshToken":  "eyJjdHkiOiJKV1Q...",
  "ExpiresIn":     3600,
  "TokenType":     "Bearer",
  "DeviceKey":      null,
  "DeviceGroupKey": null,
  "mfa_settings":  { "required": false, ... }
}
Store the three tokens and the expiry. Compute tokenExpirationDate = now() + ExpiresIn (seconds).

Decoding the IdToken

The IdToken is a standard JWT. Decode it client-side (e.g. with jwt-decode) to read user claims:
import { jwtDecode } from "jwt-decode";

const claims = jwtDecode<{
  sub: string;
  email: string;
  name?: string;
  "cognito:groups": string[];
  "custom:multi_partner_view"?: "name" | "id";
}>(response.IdToken);

// Extract partner memberships
const partnerIds = (claims["cognito:groups"] ?? [])
  .filter(g => /^(partner-id-.+|fd\d+)$/.test(g));
Special group markers — reject login if any are present:
  • account-disabled — the account is suspended.
  • external-user — not allowed to log into a first-party app.
Admin-only markers (you usually don’t expose admin features to partners): fd-admin, back-office-admin, webapp-access-group-<N>.

2. Make authenticated requests

curl -s "https://client-api.prd.future-demand.com/api/v3/partners/" \
  -H "Authorization: Bearer $FD_ACCESS_TOKEN" \
  -H "X-Preferred-Partner-Id: $FD_PARTNER_ID"
The reference webapp dispatches a global LOGOUT on any 401 response — no auto-retry. If your client behaves the same, make sure your token refresh runs before the request goes out (proactive), not as a 401 fallback.

3. Multi-factor authentication

MFA is mandatory for end-user logins after 2025-05-12.

Respond to a challenge

PUT /auth/challenge/mfa-token
{
  "username":         "user@partner.com",
  "session":          "<ChallengeSession from /auth>",
  "mfa_token":        "123456",
  "mfa_method":       "SOFTWARE_TOKEN_MFA" | "EMAIL_OTP",
  "remember_device":  true
}
A successful response carries the same tokens as a non-MFA login, plus DeviceKey and DeviceGroupKey if remember_device: true was set. Cache them per-user — passing them on the next /auth call lets the user skip the MFA prompt on a trusted device.

Configure MFA (post-login)

EndpointPurpose
GET /auth/mfa/settingsCurrent MFA state for the user
PUT /auth/mfa/settingsEnable / disable / set preferred MFA method
GET /auth/mfa/configure/totpGet qr_uri + qr_secret for authenticator-app enrolment
PUT /auth/mfa/configure/totpVerify the TOTP setup with a mfa_token

4. Refresh tokens

Access tokens expire after one hour. Refresh proactively when less than 5 minutes remain, not on the failed 401:
curl -s -X POST "https://client-api.prd.future-demand.com/api/v3/auth/refresh_token" \
  -H "Content-Type: application/json" \
  -d '{
    "token":            "'"$FD_REFRESH_TOKEN"'",
    "device_key":       null,
    "device_group_key": null
  }'
Response:
{ "AccessToken": "eyJ...", "IdToken": "eyJ...", "TokenType": "Bearer" }
The refresh token does not rotate. Keep using the original RefreshToken from the initial /auth call until the user signs out or it eventually expires (Cognito default: 30 days).
See Cookbook: Token refresh for a complete fetch/axios interceptor.

5. Forgot / reset password

1

Initiate

POST /auth/initiate_forgot_password
{ "email": "user@partner.com" }
User receives a confirmation code by email.
2

Confirm

POST /auth/confirm_forgot_password
{ "email": "...", "conf_code": "123456", "new_password": "newSecret!" }

6. Create users under a partner

If you’re letting partner-admins onboard users:
POST /auth/partners/{partner_id}/users
{
  "email":       "new@partner.com",
  "password":    "Temp123!",
  "given_name":  "Ada",
  "family_name": "Lovelace",
  "partner_id":  "partner-id-acme"
}
The user receives a confirmation code. Complete with POST /auth/partners/users/confirm ({email, conf_code}).

7. Logout

There is no server-side token revocation. Tokens remain valid until their natural expiry. To log a user out:
  1. Drop the access token, ID token, and refresh token from your client store.
  2. Clear any DeviceKey / DeviceGroupKey you cached for “remember device”.
If your integration is server-to-server, treat logout as “drop the cached session”.

8. Permissions and features

After login, fetch two things to drive your UI:
# Per-partner permission strings (re-fetch on partner switch)
curl -s "https://client-api.prd.future-demand.com/api/v3/permissions" \
  -H "Authorization: Bearer $FD_ACCESS_TOKEN" \
  -H "X-Preferred-Partner-Id: $FD_PARTNER_ID"

# Feature flags for this partner
curl -s "https://client-api.prd.future-demand.com/api/v3/features" \
  -H "Authorization: Bearer $FD_ACCESS_TOKEN" \
  -H "X-Preferred-Partner-Id: $FD_PARTNER_ID"
Use permissions for “can this user open the campaign editor?” decisions and features for “is the Wave wizard enabled for this partner?” toggles.

9. Error model

StatusMeaningWhat to do
401Missing/expired token, or admin marker tripped (account-disabled).Re-authenticate. The reference webapp force-logs-out on any 401.
403Authenticated but not authorised for this partner/resource.Check X-Preferred-Partner-Id matches a group the token actually has.
400Validation.Inspect validation_errors[] in the response body.
429Rate limit.Back off — see Rate limits.
Response shape on errors:
{
  "message":      "error.auth.invalid_credentials",
  "error_code":   "INVALID_CREDENTIALS",
  "error_subcode": null,
  "validation_errors": [{ "code": "..." }]
}
The message is an i18n key (always starts with error.); map it to your own copy. Log error_code and the x-request-id response header in any support ticket.

Security checklist

Access tokens are short-lived (~1h) and acceptable in a browser. The refresh token is long-lived — keep it on your backend if your UI is web-based, or use a secure storage API on native.
Even if your user belongs to only one partner. Omitting it on multi-partner accounts will scope your call to an arbitrary partner.
Track ExpiresIn and refresh when less than 5 minutes remain. Don’t refresh on 401 — your user request has already failed by then.
Always the Authorization header. URLs end up in access logs and browser history.
Use distinct credentials per environment in your CI/CD secrets. Production credentials must never appear in staging logs.