Use this file to discover all available pages before exploring further.
Users authenticate with CoinList and authorize your application — no passwords are shared with you. CoinList uses OAuth 2.0 with PKCE (Proof Key for Code Exchange) to secure the redirect flow.This recipe assumes you run a backend you control: the client secret, refresh token, and token exchange stay on the server. The React SDK handles PKCE in the browser and session hydration through getAccessToken.
Configure clientId, redirectUri, and getAccessToken so the SDK can run OAuth in the browser and read short-lived access tokens from your session API.
// app/providers.tsx"use client";import { ClientId, CoinListProvider, RedirectUri,} from "@coinlist-co/react";import type { ClientConfig } from "@coinlist-co/react";export function Providers({ children }: { children: React.ReactNode }) { const config: ClientConfig = { clientId: ClientId(process.env.NEXT_PUBLIC_COINLIST_CLIENT_ID!), redirectUri: RedirectUri(process.env.NEXT_PUBLIC_COINLIST_REDIRECT_URI!), getAccessToken: async () => { const res = await fetch("/api/coinlist/oauth/access-token", { credentials: "include", }); if (res.status === 204) return null; if (!res.ok) { throw new Error("GET /api/coinlist/oauth/access-token failed"); } const data = (await res.json()) as { value: string; expiresAt: string }; return { value: data.value, expiresAt: new Date(data.expiresAt), }; }, }; return <CoinListProvider config={config}>{children}</CoinListProvider>;}
If you omit baseUrl in ClientConfig, the SDK uses its default API base URL. Set baseUrl when you need the SDK to call a different host (for example your own backend proxy).
Use the packaged sign-in card inside any subtree that already has CoinListProvider. It calls coinlist.startOAuth() when the user clicks Sign in with CoinList.
"use client";import { CoinListSignInCard } from "@coinlist-co/react/client/components";export function SignInPanel() { return <CoinListSignInCard />;}
Importing from @coinlist-co/react/client/components pulls in the SDK styles needed for this UI.For auth gating, loading states, and OffersGrid after sign-in, see partner-demo app/page.tsx.
Create a small factory that binds createCoinListServer to your outgoing response and session store. The example below uses HTTP-only cookies; adjust to your security model.
// lib/coinlist-server.tsimport type { NextResponse } from "next/server";import type { CoinListServer } from "@coinlist-co/react/server";import { ClientId, ClientSecret, createCoinListServer, RedirectUri,} from "@coinlist-co/react/server";import { createSessionCookiesStore } from "./session-store";export function coinListServer(outgoingResponse: NextResponse): CoinListServer { return createCoinListServer({ clientId: ClientId(process.env.NEXT_PUBLIC_COINLIST_CLIENT_ID!), clientSecret: ClientSecret(process.env.COINLIST_CLIENT_SECRET!), redirectUri: RedirectUri(process.env.NEXT_PUBLIC_COINLIST_REDIRECT_URI!), sessionStore: createSessionCookiesStore(outgoingResponse), });}
// lib/session-store.tsimport { cookies } from "next/headers";import { NextResponse } from "next/server";import type { OAuthSession, SessionStore } from "@coinlist-co/react/server";import { OAuthRefreshToken } from "@coinlist-co/react/server";const COINLIST_SESSION_COOKIE = "coinlist_session";/** Copy Set-Cookie headers from the SDK scratch response onto the response you return (e.g. after refresh in `accessToken()`). */export function copyCookiesFromTo(source: NextResponse, target: NextResponse) { for (const cookie of source.cookies.getAll()) { const { name, value, ...options } = cookie; target.cookies.set(name, value, options); }}export function createSessionCookiesStore( outgoingResponse: NextResponse,): SessionStore { return { async getSession(): Promise<OAuthSession | null> { const raw = (await cookies()).get(COINLIST_SESSION_COOKIE)?.value; if (!raw) return null; let parsed: { accessToken?: { value?: string; expiresAt?: string }; refreshToken?: string; }; try { parsed = JSON.parse(raw); } catch { return null; } if (!parsed.accessToken?.value || !parsed.accessToken?.expiresAt) return null; const expiresAt = new Date(parsed.accessToken.expiresAt); if (Number.isNaN(expiresAt.getTime())) return null; return { accessToken: { value: parsed.accessToken.value, expiresAt }, ...(parsed.refreshToken ? { refreshToken: OAuthRefreshToken(parsed.refreshToken) } : {}), }; }, async setSession(session: OAuthSession | null): Promise<void> { if (session == null) { outgoingResponse.cookies.delete(COINLIST_SESSION_COOKIE); return; } outgoingResponse.cookies.set( COINLIST_SESSION_COOKIE, JSON.stringify({ accessToken: { value: session.accessToken.value, expiresAt: session.accessToken.expiresAt.toISOString(), }, ...(session.refreshToken ? { refreshToken: String(session.refreshToken) } : {}), }), { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", }, ); }, };}
On the URL you registered as redirect_uri, use useCompleteCoinListOAuth: it validates the redirect (state / PKCE via coinlist.completeOAuth), POSTs to your complete endpoint, then runs coinlist.init() so getAccessToken sees the new session.This matches partner-demo app/oauth/coinlist/callback/page.tsx:
Put this file at a path that matches NEXT_PUBLIC_COINLIST_REDIRECT_URI. Read reason on your home page (for example via useSearchParams) if you want to show OAuth errors — see partner-demo app/page.tsx.
The browser SDK calls getAccessToken whenever it needs a Bearer token. Implement this route with coinlistServer(response).accessToken() so refresh happens server-side.
// app/api/coinlist/oauth/access-token/route.tsimport { coinListServer } from "@/lib/coinlist-server";import { copyCookiesFromTo } from "@/lib/session-store";import { NextResponse } from "next/server";export async function GET() { const cookieSink = new NextResponse(null, { status: 204 }); const token = await coinListServer(cookieSink).accessToken(); if (token == null) { return cookieSink; } const response = NextResponse.json({ value: token.value, expiresAt: token.expiresAt.toISOString(), }); copyCookiesFromTo(cookieSink, response); return response;}
When accessToken() refreshes the session, the SDK may set cookies on cookieSink. copyCookiesFromTo applies those to the JSON response so the browser gets the updated session cookie.
Use the sections below only if you are not using the defaults above.
Custom sign-in control (useCoinList + startOAuth)
If you build your own button or entry point, call startOAuth() from useCoinList():
"use client";import { useCoinList } from "@coinlist-co/react";export function LoginButton() { const { coinlist } = useCoinList(); return ( <button type="button" onClick={() => void coinlist.startOAuth()}> Sign in with CoinList </button> );}
Manual callback handling (completeOAuth + fetch)
You can call coinlist.completeOAuth() yourself, then POST code and codeVerifier to /api/coinlist/oauth/complete, await coinlist.init(), and redirect. Prefer useCompleteCoinListOAuth when possible — it centralizes failure reasons and avoids duplicate work in React Strict Mode.
For non-React browser code, import createCoinListClient and ClientConfig from @coinlist-co/react or @coinlist-co/react/client/core and call the same methods (startOAuth, completeOAuth, fetchAllOffers, …). You still need a backend for token exchange in production.