This walkthrough goes from “credentials in hand” to “a working Lookout view in my own React app.” It deliberately ignores most 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 the Client API’s suggested taste clusters for that event at a sample budget.
  3. Renders the suggestions.
You’ll touch three endpoints — 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 (server-side values only — never in the browser):
FD_BASE_URL=https://client-api.stg.future-demand.com/api/v3
FD_USERNAME=[email protected]
FD_PASSWORD=...
FD_PARTNER_ID=partner-id-acme
We won’t put credentials in the browser. We proxy the API through a tiny Node server that signs in, holds the token, and injects the two required headers. 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!;

// Sign in once, cache the session. (Production: add proactive refresh —
// see /cookbook/token-refresh.)
let accessToken = "";
async function signIn() {
  const r = await fetch(`${BASE}/auth`, {
    method:  "POST",
    headers: { "Content-Type": "application/json" },
    body:    JSON.stringify({
      username: process.env.FD_USERNAME,
      password: process.env.FD_PASSWORD,
    }),
  });
  const data = await r.json();
  if (!data.AccessToken) throw new Error(`Sign-in failed: ${data.Challenge ?? r.status}`);
  accessToken = data.AccessToken;
}

app.use("/api/*", async (req, res) => {
  if (!accessToken) await signIn();
  const qs  = req.url.split("?")[1];
  const url = `${BASE}/${req.params[0]}${qs ? "?" + qs : ""}`;
  const upstream = await fetch(url, {
    method: req.method,
    headers: {
      Authorization:            `Bearer ${accessToken}`,
      "X-Preferred-Partner-Id": process.env.FD_PARTNER_ID!,
      "Content-Type":           "application/json",
    },
  });
  res.status(upstream.status)
     .type(upstream.headers.get("content-type") ?? "application/json")
     .send(await upstream.text());
});

app.listen(8787, () => console.log("FD proxy on :8787"));
Run it: 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 Authorization: Bearer … + X-Preferred-Partner-Id. The 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 });
  return r.json() as Promise<T>;
}

export const fd = {
  // List endpoints use the { items, ...pagination } envelope.
  listEvents:   (params: { limit?: number; since?: string } = {}) =>
    get<EventList>("/events/", params as any),
  getEvent:     (id: string) => get<Event>(`/events/${id}`),
  // goal is enum-style: UTILIZATION | ROAS | VISIBILITY | LINK_CLICKS
  suggestedTcs: (id: string, budget: number, goal = "UTILIZATION") =>
    get<TcSuggestion[]>(`/events/${id}/suggested_tcs`, { budget, goal }),
};

export type Event = {
  id: string;
  title: string;
  start_date_time: string;
  venue_name: string;
  hall_name: string;
  city: string;
  status: string;
};

export type EventList = {
  items: Event[];
  total: number;
  limit: number;
  page: number;
  pages: number;
  nextPage: number | null;
  prevPage: number | null;
};

// suggested_tcs returns only { tc, tc_run_id } per cluster.
export type TcSuggestion = { tc: string; tc_run_id: string };

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.items))             // ← items, not data
      .catch(e => setError(String(e)));
  }, []);

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

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

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

      {active && (
        <section>
          <h2>{active.title}</h2>
          <p>{active.venue_name}{active.hall_name ? ` · ${active.hall_name}` : ""} · {active.city}</p>
          <h3>Suggested taste clusters @ €5,000 budget</h3>
          <ol>
            {tcs.map(t => (
              <li key={`${t.tc_run_id}_${t.tc}`}>{t.tc}</li>
            ))}
          </ol>
        </section>
      )}
    </div>
  );
}
suggested_tcs returns only cluster references. For a predicted-outcome summary, follow up with GET /events/{eid}/campaigns_expected_value (returns { expected_value: "<string>" }) and render that string.

3. Run it

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 the suggested clusters 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

Token refresh

Your proxy holds an access token — refresh it proactively.

Error handling

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

Pagination

/events/ is paginated — wire up infinite scroll.

TypeScript client

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