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



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

Create a fresh authorize URL for an account whose `status` has moved to `reauth_required`. The consent flow is identical to [`oauth-url`](/docs/api/reference/social-accounts/oauth-url) - platform-domain page, your `returnUrl`, poll [`oauth-status`](/docs/api/reference/social-accounts/oauth-status) - but on success the tokens bind back to the same `socialAccountId` instead of creating a new one.

Use this when you detect `reauth_required` in [`GET /v1/projects/:id/social-accounts`](/docs/api/reference/social-accounts/list-social-accounts), or when a scheduled post fails with `CREDENTIAL_INVALID`.

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

<Parameters
  title="Body"
  rows="[
  { name: 'socialAccountId', type: 'string', required: true, description: 'Account id returned by list-social-accounts. Must be the prefixed form (`sa_<uuid>`). Account must be in this project and not soft-deleted.' },
  { name: 'returnUrl', type: 'string (URL)', required: true, description: 'Where Layers redirects after consent. Same allowlist rules as [`oauth-url`](/docs/api/reference/social-accounts/oauth-url) — host (case-insensitive) must exactly match a domain on your API key\'s allowlist. Allowlist updates are not self-service; contact your Layers account manager to add a domain.' },
  { name: 'scopes', type: 'string[]', description: 'Override the default platform scope set. Max 32 entries, each ≤ 64 chars. Same default set and override rules as [`oauth-url`](/docs/api/reference/social-accounts/oauth-url#default-scopes) — see that page for the full per-platform scope tables and override caveats.' },
]"
/>

Request bodies are strict — sending an unknown field returns `422 VALIDATION` with the offending key in `details.issues[]`.

<Callout type="info">
  This endpoint does **not** honor the `Idempotency-Key` header. Same rationale as [`oauth-url`](/docs/api/reference/social-accounts/oauth-url) — the response carries a 10-minute-lived `state` token, so caching beyond that window would replay stale URLs. Deduplicate retries client-side off your own action ID.
</Callout>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/social/reauth-url \
      -H "Authorization: Bearer lp_..." \
      -H "Content-Type: application/json" \
      -d '{
        "socialAccountId": "sa_9c1e42a0-b7f3-4e5d-a2c1-8b4f5e6c7d8e",
        "returnUrl": "https://app.gicgrowth.com/reconnect/tiktok/return"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const { authorizeUrl, state } = await layers.social.createReauthUrl({
      projectId: "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
      socialAccountId: "sa_9c1e42a0-b7f3-4e5d-a2c1-8b4f5e6c7d8e",
      returnUrl: "https://app.gicgrowth.com/reconnect/tiktok/return",
    });

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

  <Tab value="Python">
    ```python
    result = layers.social.create_reauth_url(
        project_id="prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
        social_account_id="sa_9c1e42a0-b7f3-4e5d-a2c1-8b4f5e6c7d8e",
        return_url="https://app.gicgrowth.com/reconnect/tiktok/return",
    )
    ```
  </Tab>
</Tabs>

## Response [#response]

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

Once the user completes consent, the account's `status` returns to `connected` and the existing `socialAccountId` keeps its scheduled posts and metrics history.

## End-to-end reauth flow [#end-to-end-reauth-flow]

Reauth is a three-step loop: **detect** the failing account, **mint** a fresh authorize URL, **send the user through consent**, and the existing `socialAccountId` keeps publishing.

```ts title="reconcile-needs-reauth.ts"
// Step 1 — detect by polling. (The `social_account.needs_reauth` webhook is
// registered in the catalog but not yet emitting — see the callout below
// before wiring an event-driven path.)
const { items } = await fetch(
  `https://api.layers.com/v1/projects/${projectId}/social-accounts?status=reauth_required`,
  { headers: { Authorization: `Bearer ${LAYERS_API_KEY}` } },
).then(r => r.json());

for (const account of items) {
  // Step 2 — mint. The new authorize URL preserves `account.socialAccountId`.
  const { authorizeUrl, state } = await fetch(
    `https://api.layers.com/v1/projects/${projectId}/social/reauth-url`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${LAYERS_API_KEY}`,
        "Idempotency-Key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        socialAccountId: account.socialAccountId,
        returnUrl: `${APP_BASE_URL}/reconnect/${account.platform}/return`,
      }),
    },
  ).then(r => r.json());

  // Step 3 — send the user through. Most partners do one of:
  //   - Email the user a link to `authorizeUrl` (lowest friction; works
  //     when the user isn't actively logged in).
  //   - Surface a "Reconnect TikTok" banner in the app linking to the URL.
  // The flow from here is identical to the initial OAuth handshake: the
  // user consents on tiktok.com / instagram.com, lands back at `returnUrl`,
  // and your handler polls `oauth-status/${state}` until it flips to
  // `completed`. On `completed`, the same `socialAccountId` is back to
  // `connected` — queued scheduled posts targeting it resume publishing.
  await persistReauthPending({
    socialAccountId: account.socialAccountId,
    state,
    authorizeUrl,
  });
}
```

<Callout type="info">
  **`social_account.needs_reauth` isn't emitting yet.** The event is in the [webhook catalog](/docs/api/operational/webhooks#event-catalog) — subscribing today is safe (deliveries will start once the emit hook ships) but the only working signal right now is polling `social-accounts?status=reauth_required`. Plan around polling until the event goes live.
</Callout>

<Callout type="info">
  **`reauth-url` is the right tool when the `socialAccountId` already exists.** If the account has been [revoked](/docs/api/reference/social-accounts/revoke-social-account) (`status: "disconnected"`), reauth returns `404 NOT_FOUND` — use [`oauth-url`](/docs/api/reference/social-accounts/oauth-url) to create a fresh binding instead. The new binding gets a new `socialAccountId`.
</Callout>

## Errors [#errors]

See [Errors](/docs/api/operational/errors) for the canonical envelope and full code catalog. Endpoint-specific notes:

| Status | Code                     | When                                                                                                                                                                                                                                                                                                           |
| ------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `VALIDATION`             | Request body could not be parsed as JSON, or the account's platform is not reauth-eligible (only `tiktok` and `instagram` reauth via API integration today — other platforms return this with the platform name in the message).                                                                               |
| 401    | `UNAUTHENTICATED`        | Missing or invalid `Authorization` header.                                                                                                                                                                                                                                                                     |
| 403    | `FORBIDDEN_SCOPE`        | Key lacks `social:write`.                                                                                                                                                                                                                                                                                      |
| 403    | `RETURN_URL_NOT_ALLOWED` | `returnUrl` host is not on this key's allowlist. `details.returnUrl` and `details.host` echo what you sent.                                                                                                                                                                                                    |
| 404    | `NOT_FOUND`              | Project not in your organization, account not in this project, or the account is soft-deleted. Returned (not `403`) deliberately so the API doesn't leak cross-org existence. To reconnect a disconnected account, use [`oauth-url`](/docs/api/reference/social-accounts/oauth-url) to create a fresh binding. |
| 409    | `IDEMPOTENCY_CONFLICT`   | Same `Idempotency-Key` was used earlier with a materially different request body. `details.originalRequestHash` and `details.currentRequestHash` let you diff. Mint a fresh key for the new body.                                                                                                              |
| 422    | `VALIDATION`             | Body parsed but failed schema validation: `socialAccountId` malformed (must be `sa_<uuid>`), `returnUrl` not an absolute URL, unknown field rejected by `.strict()`, `scopes` exceeds bounds. `details.issues[]` lists each failure.                                                                           |
| 429    | `RATE_LIMITED`           | Per-key write budget exhausted for endpoint class `oauth`. Honor `Retry-After`.                                                                                                                                                                                                                                |
| 500    | `INTERNAL`               | Server-side failure — typically a vault write for the PKCE verifier or a `partner_oauth_states` insert. `requestId` correlates against Layers logs.                                                                                                                                                            |
| 503    | `KILL_SWITCH`            | Your key, your org, or the global API integration has been disabled. `details.scope` names which.                                                                                                                                                                                                              |

## See also [#see-also]

* [`GET /v1/projects/:id/social-accounts`](/docs/api/reference/social-accounts/list-social-accounts) - watch for `reauth_required`
* [`GET /v1/social/oauth-status/:state`](/docs/api/reference/social-accounts/oauth-status) - poll completion
