POST /v1/projects/:projectId/social/reauth-url
Create a reconnect URL when a social account's tokens expire or get revoked upstream.
/v1/projects/:projectId/social/reauth-url- 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.
projectIdstringrequiredProject owning the account.
socialAccountIdstringrequiredAccount id returned by list-social-accounts. Must be the prefixed form (`sa_<uuid>`). Account must be in this project and not soft-deleted.returnUrlstring (URL)requiredWhere 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.scopesstring[]optionalOverride 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
{
"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.
// 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.
Errors
See 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 partner API 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 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 partner API has been disabled. details.scope names which. |
See also
GET /v1/projects/:id/social-accounts- watch forreauth_requiredGET /v1/social/oauth-status/:state- poll completion