# Social accounts (/docs/api/concepts/social-accounts)



A social account is any platform handle your project can publish through. There are two kinds and they share one shape: **connected** accounts are owned by the end-customer and authorized via OAuth, and **leased** accounts are owned by Layers and bound to your project by Layers. Once either one lands in the project, the API treats them identically - same list endpoint, same scheduled-post target, same revoke semantics.

## Connected accounts [#connected-accounts]

The end-customer authorizes their own TikTok or Instagram account via OAuth. You create the URL, they complete the flow on the platform, they land back in your UI. The account then appears in the project's social-accounts list.

```http
POST /v1/projects/:projectId/social/oauth-url
{
  "platform": "tiktok",
  "returnUrl": "https://app.your-product.com/connect/complete",
  "usageNote": "Connect TikTok to let Acme publish videos to your account."
}
→ 200
{
  "authorizeUrl": "https://www.tiktok.com/v2/auth/authorize/?...",
  "state": "st_01HX9Y6K7EJ4T2ABCDEFHX9Y6K7EJ4T2ABCDEF",
  "expiresAt": "2026-04-18T12:30:00Z"
}
```

Redirect the user to `authorizeUrl`. When they finish, Layers redirects them back to your `returnUrl` with no extra query params on success — only failure paths append `?layers_oauth_error=<code>`. Detect completion by polling `GET /v1/social/oauth-status/:state` to pick up the new `socialAccountId`.

<Callout type="warn">
  `returnUrl` has to match your API key's allowed-return-domains allowlist exactly. Mismatches get `403 RETURN_URL_NOT_ALLOWED`. The allowlist is set when the key is created - contact Layers to add domains.
</Callout>

### What happens after a successful connect [#what-happens-after-a-successful-connect]

A successful OAuth callback does more than write a row to `social_accounts`. The following side effects fire automatically and are required for the account to be publishable:

* **Profile picture is rehosted** to a Layers-managed CDN. The platform-CDN URL (Instagram especially) rotates within hours; Layers stores a stable URL on the social account so partner UIs don't render broken avatars.
* **Per-account distribution layers are provisioned.** Layers initializes the system "Social Account Onboarding" template, which creates four per-account scheduling layers — Account Health Monitor, Social Distribution, Social Engagement, and Account Warmup — and attaches them to the new account. These are required for `globalPlatformPostsSync` to pick the account up and for scheduled posts targeting this account to publish.
* **Project's Social Content layer is ensured.** If the project doesn't already have a Social Content layer, one is created so the new distribution layers have a content source to draw from.
* **Onboarding-task event fires** for the org's checklist (`tiktok.connected` or `instagram.connected`).

All steps are idempotent and best-effort: a failure on any one of them logs but does not fail the OAuth flow. By the time `oauth-status` returns `completed`, the social account is connected and the layer fan-out has been queued. Reauth flows skip these side effects — the layers and onboarding-task event already exist for the account.

### Reconnecting [#reconnecting]

Platform tokens expire and platforms invalidate them for reasons outside your control. When a token goes bad, the social account's `status` flips to `reauth_required` (the webhook that fires is named `social_account.needs_reauth` - they describe the same event from two angles). Use `POST /v1/projects/:projectId/social/reauth-url` with `{ "socialAccountId": "sa_9c1e42a0...", "scopes": ["..."] }` in the body to create a reconnection URL - the handle stays the same, only the token rotates.

## Leased accounts [#leased-accounts]

Leased accounts are TikTok accounts Layers owns, warms, and rents to your project for distribution. They exist because TikTok distribution works better when content goes out on a warmed account in the right niche than on a cold end-customer account.

Submit a lease request with the target niche and count. Layers reviews the request, assigns warmed accounts when available, and the accounts appear in the social-accounts list.

```http
POST /v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/leased-accounts/request
{
  "projectId": "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
  "count": 3,
  "niche": {
    "vertical": "fitness",
    "audience": "25-40 women",
    "language": "en-US",
    "geo": ["US", "CA"]
  },
  "note": "Product launch the first week of May"
}
→ 202
{
  "requestId": "lreq_01HXB2J9FGHZMNOPQRSTUVWX",
  "status": "requested",
  "submittedAt": "2026-04-18T12:04:11.000Z"
}
```

Request routes are project-scoped. Body `projectId` must match the path. Poll `GET /v1/projects/:projectId/leased-accounts/requests/:requestId` for status - or subscribe to the [`lease_request.assigned`](/docs/api/operational/webhooks) webhook to stop polling. States today: `requested` → `assigned` (or `partial` / `rejected`). Finer-grained intermediate states (`in_review`, `provisioning`, `failed`) are planned.

### Billing [#billing]

Assigned leased accounts are billed per-account against the org's wallet. The monthly price for each account is returned as `monthlyPriceCents`. Billing begins on assignment, not on request. Releasing an account (`DELETE /v1/leased-accounts/:id`) stops the next renewal; it doesn't refund the current month.

## Publishing through either kind [#publishing-through-either-kind]

Once a social account is in the project - however it got there - it's a valid target for scheduling:

```http
POST /v1/content/:containerId/schedule
{
  "targets": [
    { "socialAccountId": "sa_9c1e42a0-b7f3-4e5d-a2c1-8b4f5e6c7d8e", "mode": "publish" },
    { "socialAccountId": "sa_4d8a1bf3-2c5e-4769-aa1d-3f5e8c7b9a01", "mode": "draft" }
  ],
  "scheduledFor": "2026-04-20T17:00:00Z"
}
```

`mode` is one of `publish`, `draft`, or `managed`. `publish` auto-posts to the connected platform at `scheduledFor`. `draft` pushes the asset to the creator's mobile app (TikTok inbox / Instagram SMS) for a final review before they post by hand. `managed` dispatches via the project's connected managed-distribution provider. IG placement (reels vs feed) is selected automatically from the container's `media_type`; there is no `feed`/`reels` knob on the API integration.

## Platform coverage [#platform-coverage]

Only the platforms below are wired end-to-end (UI + API integration + callback + token exchange). Anything not listed is unsupported — `platform` values outside this set are rejected with `422 VALIDATION` at the `/social/oauth-url` endpoint.

| Platform  | Connected (OAuth) | Leased |
| --------- | ----------------- | ------ |
| TikTok    | Yes               | Yes    |
| Instagram | Yes               | -      |

LinkedIn and YouTube are not implemented today and are not on a committed timeline. They will appear in this table when they ship; until then, treat them as out of scope for any partner integration plan.

## Edge cases worth pinning down [#edge-cases-worth-pinning-down]

A few situations every partner hits eventually. Working through them up-front saves debugging time later.

### Same handle, connected to two projects in the same org [#same-handle-connected-to-two-projects-in-the-same-org]

A TikTok account `@acmecoffee` can be connected to project A and project B independently. Layers represents the platform identity (the TikTok user `acmecoffee`) once per organization, then issues a &#x2A;*distinct `socialAccountId`** per project that binds the platform identity to that project's scheduling. The end-customer goes through OAuth consent twice (once per project), and each call returns a different `socialAccountId`.

Why it's set up this way: a scheduled post targets a `socialAccountId`, not a platform handle. Layers needs to attribute scheduled posts, metrics, and revocation actions to a specific project — so each binding is its own row, even when the underlying TikTok account is the same.

If the end-customer asks "why am I being prompted to reconnect TikTok in this second project?" — that's why. There's no shortcut today; the consent has to happen per project.

### Revoke → reconnect [#revoke--reconnect]

Calling [`DELETE /v1/social-accounts/:id`](/docs/api/reference/social-accounts/revoke-social-account) marks the old `socialAccountId` as `disconnected` permanently. If the end-customer wants back in, run them through `oauth-url` again — that creates a &#x2A;*new `socialAccountId`** (the old one stays in the system as a soft-deleted audit trail, but never reappears in the list endpoint).

Implications:

* Historical posts published under the old `socialAccountId` keep their `externalUrl` references and stay visible in your historical post lists.
* Anything you keyed off the old `socialAccountId` (analytics dashboards, customer-facing UI references) needs to be re-keyed to the new ID.
* Token rotation under reauth is a different flow — `reauth-url` preserves the `socialAccountId`. Only `revoke → oauth-url` mints a fresh ID.

### Same handle, two different organizations [#same-handle-two-different-organizations]

A TikTok user can independently consent to two partner organizations on Layers. The two organizations get completely separate views — distinct `socialAccountId`s, distinct tokens, distinct project bindings — even though TikTok itself sees the same underlying user. Layers does not deduplicate the platform identity across organizations.

This matters when one of your customers is a partner running their own integration. Your view of `@acmecoffee` and their view are isolated: separate tokens, separate scheduled posts, separate metrics syncs. The TikTok platform sees both as the same connected app sessions but doesn't expose cross-org visibility to either side.

### `reauth_required` does NOT cancel scheduled posts [#reauth_required-does-not-cancel-scheduled-posts]

When tokens go bad and an account flips to `reauth_required`, any **queued** scheduled posts stay queued. They start failing with `CREDENTIAL_INVALID` at publish time until the partner mints a [`reauth-url`](/docs/api/reference/social-accounts/reauth-url) and the user completes consent. Layers does not preemptively cancel — partners often catch reauth windows in time and the scheduled posts succeed on the recovered token.

Only `revoke` (DELETE) actively cancels queued posts. The cancellation count comes back as `canceledScheduledPosts` on the DELETE response.

## Revocation [#revocation]

`DELETE /v1/social-accounts/:id` revokes the account from the project. Effects:

* Token is invalidated at the platform where possible.
* Every queued scheduled post against this account is canceled; the response includes a `canceledScheduledPosts` count.
* For leased accounts, this is also the **release** - the account goes back into the Layers pool and stops billing.
* Historical posts (already published) stay in the project; they just won't receive new metrics syncs after token revoke.

There's no soft-delete. If you want to pause publishing without losing the connection, don't revoke - just stop scheduling new posts. The account stays in `status: "connected"` until it's explicitly revoked or its platform token dies.
