Partner = tenant
A partner is one customer of Future Demand — one organisation, one
billing relationship, one isolated dataset. Every authenticated API call is
scoped to a partner via the X-Preferred-Partner-Id header.
X-Preferred-Partner-Id: partner-id-acme
This header is always required when the user belongs to more than one
partner, and is strongly recommended to send always (so single-partner code
keeps working if you later onboard multi-partner users).
How to discover the user’s partners
After login, decode the IdToken and read cognito:groups. The groups that
match /(partner-id-.+|fd\d+)/ are partners the user belongs to:
import { jwtDecode } from "jwt-decode";
const claims = jwtDecode<{ "cognito:groups": string[] }>(idToken);
const partnerIds = claims["cognito:groups"]
.filter(g => /^(partner-id-.+|fd\d+)$/.test(g));
// e.g. ["partner-id-acme", "partner-id-beta"]
If the user has one partner, set it on every request. If multiple, your UI
should let the user pick (the reference webapp shows a PartnerSelect after
login). Persist the choice client-side and re-send on every reload.
Fetching the partner record
Once you’ve picked a partner, the canonical “who am I scoped to” call is:
curl -s "$BASE/partners/" \
-H "Authorization: Bearer $FD_ACCESS_TOKEN" \
-H "X-Preferred-Partner-Id: $FD_PARTNER_ID"
Response (abbreviated):
{
"id": "partner-id-acme",
"name": "Acme Concerts",
"domain": "acme.com",
"currency": "EUR",
"vertical": "live_entertainment",
"demand_prediction": true,
"campaign_recommendations": true,
"status": "active",
"pricing_tier": "growth",
"campaign_pricing_model": "...",
"ner_active": false
}
The boolean flags drive which products are available. Two common ones:
| Flag | Meaning |
|---|
demand_prediction | Lookout is enabled for this partner. |
campaign_recommendations | Wave is enabled for this partner. |
Gate your UI on those.
Permissions and features
Two further endpoints, both partner-scoped:
# Per-user permission strings for the currently selected partner
curl -s "$BASE/permissions" \
-H "Authorization: Bearer $FD_ACCESS_TOKEN" \
-H "X-Preferred-Partner-Id: $FD_PARTNER_ID"
# Per-partner feature flags
curl -s "$BASE/features" \
-H "Authorization: Bearer $FD_ACCESS_TOKEN" \
-H "X-Preferred-Partner-Id: $FD_PARTNER_ID"
The reference webapp gates clickability on useHasPermission(...) for things
like Wave access (Permissions.wave) and Package Builder (Permissions.backhaul).
Mirror that.
Re-fetch both on partner switch. If a multi-partner user changes their
selected partner, the previous permissions / features arrays are no
longer valid.
Why client_id shows up sometimes
The backend uses two names for the same concept in different tables:
partner_id — the public, stable identifier (partner-id-acme,
fd123). Use this everywhere you can.
client_id — an internal numeric/string id returned by GET /partners/
as result.data.id. A few legacy endpoints still expect it.
The reference webapp stores both in localStorage.partnerDetails and uses
whichever the calling endpoint demands. For new integrations, treat
partner_id as canonical — every endpoint documented in the API
Reference accepts it. If you hit a call that requires client_id explicitly,
read it from the /partners/ response.
Multi-partner users in practice
type PartnerCtx = { partnerId: string; partnerName: string };
function setPartner(p: PartnerCtx) {
localStorage.setItem("fd:partner", JSON.stringify(p));
apiClient.defaults.headers["X-Preferred-Partner-Id"] = p.partnerId;
// Re-fetch partner-scoped data
refetchPermissions();
refetchFeatures();
invalidateAllPartnerScopedQueries();
}
If you use React Query or SWR, use the partner id as part of every query key
so cached data is partitioned per partner. That removes a whole class of
“cross-partner data leak” bugs from your UI.
Admin partners and the fd-admin group
Future Demand staff accounts carry the fd-admin group and can act as any
partner. The endpoints under partner_admin (POST /partners/, PUT /partners/,
/auth/users/csv_export, PUT /rest_demo_data/...) are admin-only and not
intended for partner integrations. They are documented in the API Reference
for completeness, but should never appear in your UI.