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 package builder 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.