Client connection link

Agencies often cannot log in to a client's ad account. Instead of asking for passwords, send a link: the client opens it, signs in with the ad platform, grants read-only access and picks the account.

POST /v1/projects/{project}/ad-connect-links
{ "platforms": ["meta_ads", "google_ads"], "expiresInDays": 7 }

Answer: { link, url, code, expiresAt }. url is the public page /connect/<code> of the dashboard. Scope ads:write. List the project's links with GET (ads:read), revoke one with DELETE /v1/projects/{project}/ad-connect-links/{id}.

  • Valid 1–14 days (default 7), each platform can be connected once per link.
  • The code is 16 characters (80 bits) and stored only as a hash. Unknown, expired and revoked codes all show the same page ("This link is invalid or has expired. Please ask your agency for a new one."), so nobody can probe for codes.
  • The page is rate-limited per IP and per code.

What the client sees

  1. "Your agency would like to see Client's ad spend. You only grant read access, no password."
  2. Pick the platform → the platform's own sign-in and consent (read-only).
  3. Pick the ad account.
  4. "Done. Your agency will check briefly and then see your costs."

The sign-in state is bound to the link, platform and project; a callback for another link or after expiry is refused.

The page talks to four public endpoints (no credentials – the code is the authorization):

EndpointStep
GET /v1/connect/{code}what the link offers (agency, client, platforms still open)
POST /v1/connect/{code}/startstarts the read-only OAuth of one platform (reserved once per platform)
POST /v1/connect/{code}/callbackOAuth return: verifies the state bound to the code, lists the reachable accounts
POST /v1/connect/{code}/completehands over the chosen accounts – they wait for your confirmation

Confirm the account

A client-connected account starts as waiting for confirmation. Its costs sync right away (including history), but they do not count in reports, cost per lead or the budget until you confirm:

POST /v1/projects/{project}/ad-accounts/{platform}/{accountId}/confirm
POST /v1/projects/{project}/ad-accounts/{platform}/{accountId}/reject

Scope ads:write. The dashboard shows the account name, id, currency and the last 30 days' spend as a plausibility check, plus "Wrong account? Reject". Until then members with ads:write see the setup alert "Client connected Meta · confirm account". Both steps are audited (ads.client_link_used, ads.account_confirmed).