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



<Endpoint method="POST" path="/v1/projects/:projectId/social/oauth-url" auth="Bearer" scope="social:write" phase="1" />

Create an OAuth authorization URL for TikTok or Instagram. The end-customer opens it in a top-level browser tab, consents on the platform's domain, and is redirected back to a `returnUrl` you control — not to Layers.

You pass `returnUrl` per call. It must match an allowlist on your API key; anything else is rejected with `RETURN_URL_NOT_ALLOWED`. Layers never re-hosts the consent page, and the URL cannot be framed (TikTok and Instagram block their domains from iframes by ToS). Your UI opens it in a new tab or redirects the user to it directly.

<Callout type="info">
  The authorize URL lives on `tiktok.com` or `instagram.com`. It will not render inside an iframe. Open it in a top-level tab, then poll [`GET /v1/social/oauth-status/:state`](/docs/api/reference/social-accounts/oauth-status) until it flips to `completed`. The status endpoint is not project-scoped — the state token uniquely identifies the attempt and is scoped by API key.
</Callout>

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string', required: true, description: 'Project that will own the resulting social account.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'platform', type: 'string', required: true, enum: ['tiktok', 'instagram'], description: 'Target platform.' },
  { name: 'returnUrl', type: 'string (URL)', required: true, description: 'Where Layers redirects the end-customer after consent. Must match an entry in the key\'s return_url_allowlist.' },
  { name: 'scopes', type: 'string[]', description: 'Platform scopes to request. Defaults to the full set Layers needs for publishing and metrics.' },
  { name: 'usageNote', type: 'string', description: 'Shown to the user on your side after the callback (if you render one). Opaque to Layers.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/projects/prj_01HX9Y7K8M2P4RSTUV56789AB/social/oauth-url \
      -H "Authorization: Bearer lp_live_01HX9Y6K7EJ4T2_4QZpN..." \
      -H "Content-Type: application/json" \
      -d '{
        "platform": "tiktok",
        "returnUrl": "https://app.gicgrowth.com/connect/tiktok/return",
        "usageNote": "Connecting Acme Coffee"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const { authorizeUrl, state } = await layers.social.createOAuthUrl({
      projectId: "prj_01HX9Y7K8M2P4RSTUV56789AB",
      platform: "tiktok",
      returnUrl: "https://app.gicgrowth.com/connect/tiktok/return",
      usageNote: "Connecting Acme Coffee",
    });

    window.location.assign(authorizeUrl);
    ```
  </Tab>

  <Tab value="Python">
    ```python
    result = layers.social.create_oauth_url(
        project_id="prj_01HX9Y7K8M2P4RSTUV56789AB",
        platform="tiktok",
        return_url="https://app.gicgrowth.com/connect/tiktok/return",
        usage_note="Connecting Acme Coffee",
    )
    # send result["authorizeUrl"] to the end-customer's browser
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="OAuth URL created">
  ```json
  {
    "authorizeUrl": "https://www.tiktok.com/v2/auth/authorize?client_key=...&state=st_01HXZ8...",
    "state": "st_01HXZ8K2M4P5QRS6TUV7WXYZ9A",
    "expiresAt": "2026-04-18T19:12:11Z"
  }
  ```
</Response>

The returned `state` is a Layers-created opaque token — do not reuse or modify it. When the end-customer returns to your `returnUrl`, Layers appends `?layers_oauth_error=<code>` only on failure (the success case carries no extra params — Layers has already persisted the social account). Pass the same `state` to [`GET /v1/social/oauth-status/:state`](/docs/api/reference/social-accounts/oauth-status) to learn the resulting `socialAccountId`.

The authorize URL expires in 10 minutes. After expiry, create a new one — do not cache.

## Errors [#errors]

| Status | Code                     | When                                                                                |
| ------ | ------------------------ | ----------------------------------------------------------------------------------- |
| 400    | `VALIDATION`             | `platform` not recognized, `returnUrl` not an absolute URL.                         |
| 401    | `UNAUTHENTICATED`        | Missing or invalid key.                                                             |
| 403    | `FORBIDDEN_SCOPE`        | Key lacks `social:write`.                                                           |
| 403    | `RETURN_URL_NOT_ALLOWED` | `returnUrl` not in the key's `return_url_allowlist`. Add it via ladmin, then retry. |
| 404    | `NOT_FOUND`              | Project not in your organization.                                                   |
| 429    | `RATE_LIMITED`           | Write budget exhausted.                                                             |

## See also [#see-also]

* [`GET /v1/social/oauth-status/:state`](/docs/api/reference/social-accounts/oauth-status) — poll completion (not project-scoped)
* [`GET /v1/projects/:projectId/social-accounts`](/docs/api/reference/social-accounts/list-social-accounts) — list connected accounts
* [`POST /v1/projects/:projectId/social/reauth-url`](/docs/api/reference/social-accounts/reauth-url) — refresh an existing connection
* [Social accounts](/docs/api/concepts/social-accounts) — how Layers stores and refreshes tokens
