This walkthrough goes from “API key in hand” to “a working Lookout card in my own React app.” It deliberately ignores 90% of the API surface so you can ship something today and grow from there.

What you’ll build

A single React page that:
  1. Lists upcoming events for your partner.
  2. When the user clicks one, fetches Affinity’s suggested taste clusters for that event at a sample budget.
  3. Renders the top three suggestions with expected value.
You’ll touch four endpoints — that’s enough to prove the integration end to end.

0. Setup

npm create vite@latest my-fd-integration -- --template react-ts
cd my-fd-integration
npm i
Add a .env.local:
VITE_FD_BASE_URL=https://client-api.stg.future-demand.com/api/v3
# Do NOT put your API key here — see below.
We won’t put the API key in the browser. Instead, we’ll proxy the API through a tiny Node server. For a real partner integration, this is non-negotiable.
Create server.ts:
server.ts
import express from "express";
import fetch from "node-fetch";

const app = express();
const BASE = process.env.FD_BASE_URL!;
const KEY  = process.env.FD_API_KEY!;

app.use("/api/*", async (req, res) => {
  const upstream = await fetch(BASE + req.params[0] + (req.url.split("?")[1] ? "?" + req.url.split("?")[1] : ""), {
    method: req.method,
    headers: { apikey: KEY, "Content-Type": "application/json" },
  });
  const body = await upstream.text();
  res.status(upstream.status).type(upstream.headers.get("content-type") ?? "application/json").send(body);
});

app.listen(8787, () => console.log("FD proxy on :8787"));
Run it: FD_API_KEY=... FD_BASE_URL=... node --loader ts-node/esm server.ts. Point Vite’s dev server at it via vite.config.ts:
import { defineConfig } from "vite";
export default defineConfig({
  server: { proxy: { "/api": "http://localhost:8787" } },
});
Now the browser hits /api/... and the proxy injects your API key. Same pattern works behind Next.js, Remix, Express, or anything else.

1. The tiny client

src/fd.ts
const BASE = "/api"; // proxied to FD by the Node server

async function get<T>(path: string, qs?: Record<string, string | number>): Promise<T> {
  const url = qs ? `${BASE}${path}?${new URLSearchParams(qs as any)}` : `${BASE}${path}`;
  const r = await fetch(url);
  if (!r.ok) throw Object.assign(new Error(`FD ${r.status}`), { status: r.status, body: await r.text() });
  return r.json() as Promise<T>;
}

export const fd = {
  listEvents:    (params: { limit?: number; since?: string } = {}) =>
    get<{ data: Event[]; pagination: Pagination }>("/events/", params as any),
  getEvent:      (id: string) =>
    get<Event>(`/events/${id}`),
  suggestedTcs:  (id: string, budget: number, goal: "tickets" | "revenue" = "tickets") =>
    get<TcSuggestion[]>(`/events/${id}/suggested_tcs`, { budget, goal }),
  expectedValue: (id: string, budget: number, goal: "tickets" | "revenue" = "tickets") =>
    get<ExpectedValue[]>(`/events/${id}/campaigns_expected_value`, { budget, goal }),
};

export type Event = { id: string; name: string; start_at: string; venue: string; currency: string; status: string };
export type TcSuggestion = { tc: string; label: string; score: number; reach_estimate: number };
export type ExpectedValue = { tc: string; expected_tickets: number; expected_revenue: number };
export type Pagination = { page: number; limit: number; total: number };

2. The page

src/App.tsx
import { useEffect, useState } from "react";
import { fd, type Event, type TcSuggestion } from "./fd";

export default function App() {
  const [events, setEvents]   = useState<Event[]>([]);
  const [active, setActive]   = useState<Event | null>(null);
  const [tcs, setTcs]         = useState<TcSuggestion[]>([]);
  const [error, setError]     = useState<string | null>(null);

  useEffect(() => {
    fd.listEvents({ limit: 20 })
      .then(r => setEvents(r.data))
      .catch(e => setError(String(e)));
  }, []);

  useEffect(() => {
    if (!active) return;
    fd.suggestedTcs(active.id, 5000, "tickets")
      .then(setTcs)
      .catch(e => setError(String(e)));
  }, [active]);

  if (error) return <pre style={{ color: "crimson" }}>{error}</pre>;

  return (
    <div style={{ display: "grid", gridTemplateColumns: "300px 1fr", gap: 16, padding: 16 }}>
      <ul>
        {events.map(e => (
          <li key={e.id}>
            <button onClick={() => setActive(e)}>{e.name}</button>
            <small>{new Date(e.start_at).toLocaleDateString()}</small>
          </li>
        ))}
      </ul>

      {active && (
        <section>
          <h2>{active.name}</h2>
          <p>{active.venue}</p>
          <h3>Suggested taste clusters @ €5,000 budget</h3>
          <ol>
            {tcs.slice(0, 3).map(t => (
              <li key={t.tc}>
                <strong>{t.label}</strong> — score {t.score.toFixed(2)},
                reach ~{t.reach_estimate.toLocaleString()}
              </li>
            ))}
          </ol>
        </section>
      )}
    </div>
  );
}

3. Run it

FD_API_KEY=... FD_BASE_URL=https://client-api.stg.future-demand.com/api/v3 node --loader ts-node/esm server.ts &
npm run dev
Open http://localhost:5173. You should see your events list on the left, and clicking one renders three Affinity suggestions on the right.

What you’ve just done

You’ve built a minimal Lookout integration. The same pattern scales to the full app:

Next: harden it

Error handling

Map 401/403/429 to retry, refresh, or surface to the user.

Pagination

/events/ is paginated — wire up infinite scroll or cursor paging.

Token refresh

If you switch from API key to user-token auth, you need this.

TypeScript client

A pre-generated client (from the OpenAPI spec) you can drop in.