# 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 row shows up 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/auth/authorize?...",
  "state": "st_01HX9Y6K7EJ4T2ABCDEFHX9Y6K7EJ4T2ABCDEF",
  "expiresAt": "2026-04-18T12:30:00Z"
}
```

Redirect the user to `authorizeUrl`. When they finish, Layers' callback writes the `social_accounts` row and 302s them back to your `returnUrl` with `?layers_state=<state>&status=success`. You then call `GET /v1/social/oauth-status/:state` (not project-scoped — the `state` token itself scopes the lookup) 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>

### 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_...", "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.

The important thing to know up front: &#x2A;*provisioning is a manual admin function at Layers, permanently.** Your API submits a lease request; Layers reviews the niche, allocates a warmed account, and binds it to your project by inserting a `social_accounts` row. Your agent then sees it appear in the list call.

```http
POST /v1/projects/254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/leased-accounts/request
{
  "projectId": "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 set by Layers at assignment time and surfaced on the account row 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_01HX9Y6K7EJ4T2ABCDEF...", "mode": "direct_publish" },
    { "socialAccountId": "sa_01HY9Y6K7EJ4T2ABCDEF...", "mode": "draft_to_device" }
  ],
  "scheduledFor": "2026-04-20T17:00:00Z"
}
```

Valid modes depend on the platform. `direct_publish` is the default; TikTok also supports `draft_to_device` (creator reviews on the mobile app before posting); Instagram supports `reels` and `feed` as shape variants.

## Platform coverage [#platform-coverage]

| Platform  | Connected (OAuth) | Leased (ops-provisioned) |
| --------- | ----------------- | ------------------------ |
| TikTok    | Yes               | Yes                      |
| Instagram | Yes               | —                        |
| LinkedIn  | Planned           | —                        |
| YouTube   | Planned           | —                        |

LinkedIn and YouTube depend on platform-side approval timelines; timing isn't committed yet.

## 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.
