# POST /v1/projects/:projectId/ads/ad-accounts/oauth-url (/docs/api/reference/ads/ad-accounts-oauth-url)



<Endpoint method="POST" path="/v1/projects/{projectId}/ads/ad-accounts/oauth-url" auth="Bearer" scope="ads:write" phase="1" />

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.

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string (UUID)', required: true, description: 'Project the new ad account will be attached to.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'platform', type: 'string', required: true, description: 'Which platform to connect.', enum: ['meta_ads', 'tiktok_ads', 'apple_ads'] },
  { name: 'redirectUrl', type: 'string (URL)', required: true, description: '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.' },
  { name: 'state', type: 'string', description: 'Opaque string echoed back in the redirect. Use it to round-trip your own request id.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    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"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    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`.
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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()
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="OK — URL valid for 10 minutes.">
  ```json
  {
    "url": "https://www.facebook.com/v19.0/dialog/oauth?client_id=...&redirect_uri=...&state=...",
    "expiresAt": "2026-04-18T19:32:00Z",
    "platform": "meta_ads"
  }
  ```
</Response>

<Response status="403" description="redirectUrl host not on the API key's allowedReturnDomains.">
  ```json
  {
    "error": {
      "code": "RETURN_URL_NOT_ALLOWED",
      "message": "returnUrl rejected — api_keys.scope.allowedReturnDomains is empty. Add the FQDN in ladmin and retry.",
      "requestId": "req_..."
    }
  }
  ```
</Response>

<Response status="400" description="Validation — redirectUrl is not a parseable absolute URL, platform missing or unsupported.">
  ```json
  {
    "error": {
      "code": "VALIDATION",
      "message": "Invalid oauth-url body.",
      "requestId": "req_..."
    }
  }
  ```
</Response>

<Response status="404" description="Project does not exist in your organization." />

## Redirect flow [#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}`](/docs/api/reference/ads/list-ad-accounts) 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 [#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 Layers creates no ad account. You will see no new row on poll — that is your signal the user rejected.

## Notes [#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 [#see-also]

* [`GET /v1/projects/:projectId/ads/ad-accounts`](/docs/api/reference/ads/list-ad-accounts) — poll for the new account
* [Onboard a customer](/docs/api/guides/onboard-customer) — full connection walkthrough
* [Authentication](/docs/api/getting-started/authentication) — Partner keys, redirect origins, scopes
