POST /v1/projects/:projectId/ads/ad-accounts/oauth-url
Create a one-time OAuth URL for connecting a Meta, TikTok, or Apple ad account to a project.
POST
/v1/projects/{projectId}/ads/ad-accounts/oauth-urlPhase 1stable
- Auth
- Bearer
- Scope
- ads:write
Returns a short-lived OAuth URL your user opens to grant Layers access to their ad account. Layers' ad platform integrations are bring-your-own - this is the only path to connect an ad account today. The URL itself is single-use and scoped to one project, one platform, and one user session.
Path
projectIdstring (UUID)requiredProject the new ad account will be attached to.
Body
platformstringrequiredWhich platform to connect.One of:meta_ads,tiktok_ads,apple_adsreturnUrlstring (URL)requiredWhere to send the user after they complete (or reject) the OAuth flow. Must be HTTPS and match an allowed return URL on your API key. Same field name as `/social/oauth-url`.redirectUrlstring (URL)optionalDeprecated alias for `returnUrl`. Accepted for backwards compatibility for one release cycle and then removed. Send `returnUrl` going forward.statestringoptionalOpaque string echoed back in the redirect. Use it to round-trip your own request id.
Example request
curl -X POST https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/ads/ad-accounts/oauth-url \
-H "Authorization: Bearer lp_..." \
-H "Content-Type: application/json" \
-d '{
"platform": "meta_ads",
"returnUrl": "https://app.example.com/connect/meta/complete",
"state": "sess_9f2a4d1c"
}'const res = await fetch(
`https://api.layers.com/v1/projects/${projectId}/ads/ad-accounts/oauth-url`,
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
platform: "meta_ads",
returnUrl: "https://app.example.com/connect/meta/complete",
state: crypto.randomUUID(),
}),
},
);
const { url, expiresAt } = await res.json();
// Redirect the user to `url`.import httpx, uuid
r = httpx.post(
f"https://api.layers.com/v1/projects/{project_id}/ads/ad-accounts/oauth-url",
headers={"Authorization": f"Bearer {api_key}"},
json={
"platform": "meta_ads",
"returnUrl": "https://app.example.com/connect/meta/complete",
"state": str(uuid.uuid4()),
},
)
payload = r.json()Response
200OK - URL created.
{
"url": "https://www.facebook.com/v19.0/dialog/oauth?client_id=...&redirect_uri=...&state=...",
"expiresAt": "2026-04-18T19:32:00Z",
"platform": "meta_ads"
}403returnUrl host is not on this API key's return-URL allowlist.
{
"error": {
"code": "RETURN_URL_NOT_ALLOWED",
"message": "Return URL is not on this key's allowlist. Contact your Layers account manager to add this domain to the key's allowed return URLs.",
"requestId": "req_...",
"details": {
"returnUrl": "https://app.example.com/connect/meta/complete",
"host": "app.example.com"
}
}
}422Validation - returnUrl is not a parseable absolute URL, platform missing or unsupported.
{
"error": {
"code": "VALIDATION",
"message": "Invalid oauth-url body.",
"requestId": "req_..."
}
}404Project does not exist in your organization.
Redirect flow
- Your user clicks "Connect Meta Ads" in your UI.
- You call this endpoint and receive
url. - You redirect the user to
url. - The user grants access on the platform.
- The platform redirects to a Layers-hosted callback (
/v1/ads/oauth-callback/{platform}). - Layers completes the token exchange and redirects the browser to your
returnUrl(with?layers_oauth_error=<code>only on failure - the success path redirects cleanly). - Poll
GET /v1/projects/:projectId/ads/ad-accounts?platforms={platform}until the account appears withtokenStatus: "valid".
Your state is kept for callback correlation but is not echoed in the redirect query string.
Scope Layers requests
| Platform | Scopes |
|---|---|
meta_ads | ads_management, ads_read, business_management, pages_manage_ads, pages_show_list |
tiktok_ads | user.info.basic, advertiser.list, ad.read, ad.write |
apple_ads | campaigns:read, campaigns:write |
If the user denies any required scope, the redirect still fires and no ad account is connected. You will see no new account on poll - that is your signal the user rejected.
Notes
urlis single-use and short-lived. Create a fresh one if the user abandons and comes back later.- Reconnecting an expired account uses the same endpoint and updates the existing connection rather than creating a new one.
returnUrlhost must be on the API key's return-URL allowlist. Contact your Layers account manager to add a new domain — partners cannot self-serve the allowlist today. The legacy field nameredirectUrlis accepted for one release cycle as a deprecated alias; new integrations should sendreturnUrl.- There is no "connect and immediately use" shortcut. Wait for
tokenStatus: "valid"before making campaign/ads calls against the new ad account.
See also
GET /v1/projects/:projectId/ads/ad-accounts- poll for the new account- Getting started - the canonical partner flow (project → influencer → content → schedule)
- Authentication - Partner keys, redirect origins, scopes