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:
Lists upcoming events for your partner.
When the user clicks one, fetches Affinity’s suggested taste clusters
for that event at a sample budget.
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:
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
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
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.