Layers

POST /v1/projects/:projectId/social/reauth-url

Create a reconnect URL when a social account's tokens expire or get revoked upstream.

View as Markdown
POST/v1/projects/:projectId/social/reauth-url
Phase 1stable
Auth
Bearer
Scope
social:write

Create a fresh authorize URL for an account whose status has moved to reauth_required. The consent flow is identical to oauth-url - platform-domain page, your returnUrl, poll 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, or when a scheduled post fails with CREDENTIAL_INVALID.

Path
  • projectId
    stringrequired
    Project owning the account.
Body
  • socialAccountId
    stringrequired
    Account id returned by list-social-accounts. Must be the prefixed form (`sa_<uuid>`). Account must be in this project and not soft-deleted.
  • returnUrl
    string (URL)required
    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.
  • scopes
    string[]optional
    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[].

This endpoint does not honor the Idempotency-Key header. Same rationale as 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.

Example request

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"
  }'
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);
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",
)

Response

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

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

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.

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,
  });
}

social_account.needs_reauth isn't emitting yet. The event is in the webhook 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.

reauth-url is the right tool when the socialAccountId already exists. If the account has been revoked (status: "disconnected"), reauth returns 404 NOT_FOUND — use oauth-url to create a fresh binding instead. The new binding gets a new socialAccountId.

Errors

See Errors for the canonical envelope and full code catalog. Endpoint-specific notes:

StatusCodeWhen
400VALIDATIONRequest body could not be parsed as JSON, or the account's platform is not reauth-eligible (only tiktok and instagram reauth via partner API today — other platforms return this with the platform name in the message).
401UNAUTHENTICATEDMissing or invalid Authorization header.
403FORBIDDEN_SCOPEKey lacks social:write.
403RETURN_URL_NOT_ALLOWEDreturnUrl host is not on this key's allowlist. details.returnUrl and details.host echo what you sent.
404NOT_FOUNDProject 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 to create a fresh binding.
409IDEMPOTENCY_CONFLICTSame 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.
422VALIDATIONBody 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.
429RATE_LIMITEDPer-key write budget exhausted for endpoint class oauth. Honor Retry-After.
500INTERNALServer-side failure — typically a vault write for the PKCE verifier or a partner_oauth_states insert. requestId correlates against Layers logs.
503KILL_SWITCHYour key, your org, or the global partner API has been disabled. details.scope names which.

See also

On this page