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.
Create a link
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
- "Your agency would like to see Client's ad spend. You only grant read access, no password."
- Pick the platform → the platform's own sign-in and consent (read-only).
- Pick the ad account.
- "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):
| Endpoint | Step |
|---|---|
GET /v1/connect/{code} | what the link offers (agency, client, platforms still open) |
POST /v1/connect/{code}/start | starts the read-only OAuth of one platform (reserved once per platform) |
POST /v1/connect/{code}/callback | OAuth return: verifies the state bound to the code, lists the reachable accounts |
POST /v1/connect/{code}/complete | hands 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}/rejectScope 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).