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.

Files

src/fd/types.ts — generated

npx openapi-typescript https://docs.future-demand.com/api-reference/openapi.json \
  -o src/fd/types.ts
Regenerate nightly in CI. See the CI workflow snippet.

src/fd/session.ts — sign-in + refresh

const BASE = process.env.FD_BASE_URL!;
const REFRESH_THRESHOLD_MS = 5 * 60 * 1000;

export type Session = {
  accessToken:  string;
  refreshToken: string;
  expiresAt:    number;   // ms epoch
  partnerId:    string;
};

let session: Session | null = null;
let refreshPromise: Promise<Session> | null = null;

export async function signIn(
  username: string,
  password: string,
  partnerId: string,
): Promise<Session> {
  const r = await fetch(`${BASE}/auth`, {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify({ username, password }),
  });
  if (!r.ok) throw new Error(`Sign-in failed: ${r.status}`);
  const data = await r.json();
  if (!data.AccessToken)
    throw new Error(`Sign-in challenge: ${data.Challenge ?? "unknown"}`);

  session = {
    accessToken:  data.AccessToken,
    refreshToken: data.RefreshToken,
    expiresAt:    Date.now() + (data.ExpiresIn ?? 3600) * 1000,
    partnerId,
  };
  return session;
}

async function refresh(): Promise<Session> {
  if (!session) throw new Error("No session");
  const r = await fetch(`${BASE}/auth/refresh_token`, {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify({ token: session.refreshToken }),
  });
  if (!r.ok) throw new Error(`Refresh failed: ${r.status}`);
  const data = await r.json();
  session = {
    ...session,
    accessToken: data.AccessToken,
    expiresAt:   Date.now() + (data.ExpiresIn ?? 3600) * 1000,
  };
  return session;
}

export async function getSession(): Promise<Session> {
  if (!session) throw new Error("Not signed in");
  if (session.expiresAt - Date.now() > REFRESH_THRESHOLD_MS) return session;
  refreshPromise ??= refresh().finally(() => { refreshPromise = null; });
  return refreshPromise;
}

export function setPartnerId(partnerId: string) {
  if (!session) throw new Error("Not signed in");
  session = { ...session, partnerId };
}

export function clearSession() { session = null; refreshPromise = null; }

src/fd/client.ts — the typed client

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 backoffMiddleware: Middleware = {
  async onResponse({ request, response }) {
    if (response.status !== 429 && response.status < 500) return response;
    const retryAfter = Number(response.headers.get("Retry-After")) || 5;
    await new Promise(r => setTimeout(r, retryAfter * 1000));
    return fetch(request.url, { method: request.method, headers: request.headers, body: request.body });
  },
};

export const fd = createClient<paths>({ baseUrl: BASE });
fd.use(authMiddleware);
fd.use(backoffMiddleware);

// Optional: a second client for the CR API host
export const fdCr = createClient<any>({ baseUrl: process.env.FD_CR_BASE_URL! });
fdCr.use(authMiddleware);

Usage

import { fd, signIn, setPartnerId } from "./fd";

await signIn("you@partner.com", "...", "partner-id-acme");

// Strongly typed — autocomplete on path, query, response
const { data: events, error } = await fd.GET("/events/", {
  params: { query: { limit: 5, descending: false } },
});

const { data: tcs } = await fd.GET("/events/{id}/suggested_tcs", {
  params: {
    path:  { id: events!.items[0].id },
    query: { budget: 5000, goal: "tickets" },
  },
});

Server-side variant

In a Node server you’ll typically:
  1. Sign in once at startup with a service account.
  2. Run a background timer that refreshes ~5 min before expiry.
  3. Switch partnerId per inbound request based on the authenticated user.
import { signIn, getSession, setPartnerId } from "./fd/session";

await signIn(SVC_USER, SVC_PASS, INITIAL_PARTNER);
setInterval(() => getSession().catch(console.error), 60_000);

// Per request:
app.use(async (req, res, next) => {
  setPartnerId(req.user.partnerId);   // attach the right tenant
  next();
});

Caveats

The snippets above store session globally. In a Node server with concurrent requests, don’t call setPartnerId per request — race conditions. Either (a) pass partnerId per call to a thin wrapper, or (b) use AsyncLocalStorage to scope the session per request.
signIn above throws on Challenge. For interactive flows, handle the challenge response and call PUT /auth/challenge/mfa-token. See Authentication.
It loses the original middleware chain on retry. If you need middleware-applied retries, lift the retry logic out of middleware and into your own request helper.