Layers
Partner APIAPI referenceAds

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.

View as Markdown
POST/v1/projects/{projectId}/ads/ad-accounts/oauth-url
Phase 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
  • projectId
    string (UUID)required
    Project the new ad account will be attached to.
Body
  • platform
    stringrequired
    Which platform to connect.
    One of: meta_ads, tiktok_ads, apple_ads
  • redirectUrl
    string (URL)required
    Where to send the user after they complete (or reject) the OAuth flow. Must be HTTPS and match an allowed origin registered on your Partner credentials.
  • state
    stringoptional
    Opaque 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_01HX9Y7K8M2P4RSTUV56789AB/ads/ad-accounts/oauth-url \
  -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..." \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "meta_ads",
    "redirectUrl": "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",
      redirectUrl: "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",
        "redirectUrl": "https://app.example.com/connect/meta/complete",
        "state": str(uuid.uuid4()),
    },
)
payload = r.json()

Response

200OK — URL valid for 10 minutes.
{
  "url": "https://www.facebook.com/v19.0/dialog/oauth?client_id=...&redirect_uri=...&state=...",
  "expiresAt": "2026-04-18T19:32:00Z",
  "platform": "meta_ads"
}
403redirectUrl host not on the API key's allowedReturnDomains.
{
  "error": {
    "code": "RETURN_URL_NOT_ALLOWED",
    "message": "returnUrl rejected — api_keys.scope.allowedReturnDomains is empty. Add the FQDN in ladmin and retry.",
    "requestId": "req_..."
  }
}
400Validation — redirectUrl 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

  1. Your user clicks "Connect Meta Ads" in your UI.
  2. You call this endpoint and receive url.
  3. You redirect the user to url.
  4. The user grants access on the platform.
  5. The platform redirects to a Layers-hosted callback (/v1/ads/oauth-callback/{platform}).
  6. Layers completes the token exchange server-side, creates the credential row, and 302s the browser to your redirectUrl (with ?layers_oauth_error=<code> only on failure — the success path 302s clean).
  7. Poll GET /v1/projects/:projectId/ads/ad-accounts?platforms={platform} until the new row appears with tokenStatus: "valid". Usually under 5 seconds.

Your state is stored alongside the OAuth state row as metadata.partnerState so you can correlate the callback back to the originating session if you keep your own bookkeeping. It is not echoed in the redirect query string.

Scope Layers requests

PlatformScopes
meta_adsads_management, ads_read, business_management, pages_manage_ads, pages_show_list
tiktok_adsuser.info.basic, advertiser.list, ad.read, ad.write
apple_adscampaigns:read, campaigns:write

If the user denies any required scope, the redirect still fires and Layers creates no ad account. You will see no new row on poll — that is your signal the user rejected.

Notes

  • url is single-use and expires in 10 minutes. Create a fresh one if the user abandons and comes back later.
  • Reconnecting an expired account uses the same endpoint — the resulting row updates the existing ad_accounts entry in place rather than creating a new one.
  • redirectUrl origin must be pre-registered on your Partner credentials. Add origins in the Layers dashboard → Partner → Redirect origins.
  • There is no "connect and immediately use" shortcut. Wait for tokenStatus: "valid" before making campaign/ads calls against the new ad account.

See also

On this page