A drop-in typed client built on openapi-fetch with auth, partner header, refresh, and backoff.
The recommended client. Drop these three files into your project, set two
environment variables, and you can call any endpoint with full type safety.For a step-by-step explanation of each piece, see
Cookbook: TypeScript client generation.
import createClient, { type Middleware } from "openapi-fetch";import type { paths } from "./types";import { getSession, clearSession } from "./session";const BASE = process.env.FD_BASE_URL!;const authMiddleware: Middleware = { async onRequest({ request }) { const s = await getSession(); request.headers.set("Authorization", `Bearer ${s.accessToken}`); request.headers.set("X-Preferred-Partner-Id", s.partnerId); return request; }, async onResponse({ response }) { if (response.status === 401) clearSession(); return response; },};const authMiddleware401: Middleware = { async onResponse({ response }) { if (response.status === 401) { clearSession(); onUnauthenticated?.(); // your re-auth / redirect hook (see below) } return response; },};export const fd = createClient<paths>({ baseUrl: BASE });fd.use(authMiddleware);fd.use(authMiddleware401);// Optional: a second client for the CR API hostexport const fdCr = createClient<any>({ baseUrl: process.env.FD_CR_BASE_URL! });fdCr.use(authMiddleware);
Don’t retry inside openapi-fetch middleware. A naive
fetch(request.url, ...) retry bypasses the rest of the middleware chain
and can’t safely re-use an already-consumed request body. Put retry in a
standalone helper instead — see
Rate limits: exponential backoff
— and wrap individual calls with it:
onUnauthenticated extension point. SDK consumers usually need to
hook 401 handling into their own re-auth / redirect flow. Expose a
callback rather than hard-coding a redirect:
let onUnauthenticated: (() => void) | undefined;export function setOnUnauthenticated(fn: () => void) { onUnauthenticated = fn; }
Run a background timer that refreshes ~5 min before expiry.
Pass partnerId per request — do not mutate a global.
Never call a global setPartnerId(...) per request. With concurrent
requests, a global mutable partner id is a race condition: request A’s
partner can leak into request B’s call. Pass the partner id explicitly
to each call (a thin per-request wrapper), or scope it with
AsyncLocalStorage.
import { signIn, getSession } from "./fd/session";import createClient from "openapi-fetch";import type { paths } from "./fd/types";// One service-account session manages the token (its partnerId is unused// server-side — we override the partner header per request below).await signIn(SVC_USER, SVC_PASS, "any-partner");setInterval(() => getSession().catch(console.error), 60_000);// A per-request client bound to the caller's partner — no global mutation.function clientFor(partnerId: string) { const c = createClient<paths>({ baseUrl: process.env.FD_BASE_URL! }); c.use({ async onRequest({ request }) { const { accessToken } = await getSession(); // shared token, refreshed request.headers.set("Authorization", `Bearer ${accessToken}`); request.headers.set("X-Preferred-Partner-Id", partnerId); // per-request return request; }, }); return c;}app.get("/my-events", async (req, res) => { const fd = clientFor(req.user.partnerId); // scoped per request const { data } = await fd.GET("/events/", { params: { query: { limit: 20 } } }); res.json(data);});
With concurrent server requests, a global mutable partner id races.
Use a per-request client (above) or AsyncLocalStorage.
Retry belongs in a helper, not middleware
Don’t retry inside openapi-fetch middleware — it bypasses the
chain and can re-use a consumed body. Wrap calls with a standalone
backoff helper (see Rate limits).
refresh_token may need device keys
For some Cognito flows, POST /auth/refresh_token accepts
device_key and device_group_key alongside token. The minimal
{ token } body works for the common case, but be ready to pass the
device fields if your account uses remembered-device MFA.
MFA-enabled accounts
signIn above throws on Challenge. For interactive flows, handle
the challenge response and call PUT /auth/challenge/mfa-token. See
Authentication.