# Connect

> Run your customer through Instagram, Facebook, TikTok, YouTube, X, or Bluesky authorization without an Adeli login.

Canonical: <https://www.tryadeli.com/docs/api/connect>

The connect flow lets your customer authorize their Instagram, Facebook, TikTok, YouTube, X, or Bluesky account
from **your** application. They never see an Adeli dashboard, and you never
handle a provider token.

## How it works

1. Your server calls [`POST /api/v1/profiles/:profileId/connect`](https://www.tryadeli.com/docs/api/connect/start-connection) and gets back an
   `authUrl` plus a session id.
2. You send your customer's browser to `authUrl`. They authorize with the
   provider directly.
3. The provider returns to Adeli, which exchanges the code server-side and
   stores the token.
4. Adeli sends the browser back to your `redirectUrl` with the session id and
   status in the query string.
5. Your server [polls the session](https://www.tryadeli.com/docs/api/connect/get-connection-session) until it reaches a terminal state.

> **Warning: Open authUrl in the customer's browser**
>
> Do not fetch it from your backend. It is an authorization page that requires a
> human to grant consent; requesting it server-side will not produce a
> connection.

The session is valid for **ten minutes** and its OAuth state can be consumed
once. A callback that arrives late, twice, or with a tampered state is rejected
without re-exchanging anything with the provider.

For the full internal sequence, see the repository's
`docs/nested-oauth-connect-flow.md`.

## Endpoints

- `POST /api/v1/profiles/{profileId}/connect` — [Start connection](https://www.tryadeli.com/docs/api/connect/start-connection.md)
- `GET /api/v1/profiles/{profileId}/connect/{connectionSessionId}` — [Get connection session](https://www.tryadeli.com/docs/api/connect/get-connection-session.md)

## The browser return

When you supply a `redirectUrl`, Adeli sends the browser back with these query
parameters:

| Name                  | Type     | Required | Description                                                                                                                            |
| --------------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `connectionSessionId` | `uuid`   |          | The session to poll.                                                                                                                   |
| `status`              | `string` |          | The status at redirect time.                                                                                                           |
| `accountId`           | `uuid`   |          | Present only on success.                                                                                                               |
| `workflowId`          | `string` |          | An internal hint — `connecting-accounts` for Instagram and Facebook, `connecting-tiktok` for TikTok, `connecting-youtube` for YouTube. |

> **Note: Poll, do not trust the query string**
>
> These parameters are convenient for rendering an immediate result in your UI,
> but they arrive through the customer's browser. Confirm the outcome with a
> server-side poll before acting on it.

## Provider requirements

**Instagram** needs a professional (Business or Creator) account. Adeli requests
`instagram_business_basic`, `instagram_business_manage_comments`,
`instagram_business_manage_insights`, `instagram_business_manage_messages`, and
`instagram_business_content_publish`.

### Instagram login methods

An Instagram professional account can be connected two ways, and a profile's
Instagram connection uses exactly one of them:

- **`instagram_login`** (the default) — your customer signs in on Instagram.
  Adeli requests the `instagram_business_*` permissions above.
- **`facebook_login`** — your customer signs in with Facebook and grants the
  Facebook Page their Instagram account is linked to. Adeli requests
  `instagram_basic`, `instagram_content_publish`, `instagram_manage_comments`,
  `instagram_manage_insights`, `instagram_manage_messages`, `pages_show_list`,
  `pages_read_engagement`, `pages_manage_metadata`, and
  `business_management`. They must be able to manage messages on that Page.

Both produce the same account: posting, comments, messages, and insights work
identically, and [Accounts](https://www.tryadeli.com/docs/api/accounts) reports which method it uses.

Starting a connect with the method the profile's Instagram does **not** use
returns `409 auth_method_conflict` before your customer reaches Meta. To switch,
disconnect the account first. A Facebook Login session that cannot finish fails
with one of these `error.code` values:

| Name                          | Type | Required | Description                                                                           |
| ----------------------------- | ---- | -------- | ------------------------------------------------------------------------------------- |
| `no_instagram_account`        |      |          | None of the Pages your customer granted has an Instagram professional account linked. |
| `multiple_instagram_accounts` |      |          | More than one was granted. Start again and choose only one.                           |
| `missing_permissions`         |      |          | Facebook did not grant a permission the connection needs.                             |
| `page_messaging_task_missing` |      |          | Your customer's role on the Page cannot manage messages.                              |
| `auth_method_conflict`        |      |          | The profile was connected with Instagram Login while this session was open.           |

**Facebook** connects one Page. Adeli uses Facebook Login for Business and
requests `public_profile`, `pages_show_list`, `pages_read_engagement`,
`pages_manage_metadata`, `pages_manage_posts`, `pages_read_user_content`,
`pages_manage_engagement`, and `pages_messaging`.

Facebook is the one provider whose connection is not finished when the
authorization callback returns. Authorizing yields a _user_ token, which lists
the Pages your customer manages; a Page still has to be chosen before anything
can be published or read. Until it is, the session stays `processing` and the
redirect carries `status=pending_page_selection`. The account exists at that
point and appears in [Accounts](https://www.tryadeli.com/docs/api/accounts) with
`connectionStatus: "pending_page_selection"`, but every write against it
returns `409 page_not_selected`.

Only Pages your customer holds a direct role on are offered. A Page granted to
them solely through a business portfolio is not connectable, because Adeli does
not request `business_management`.

**TikTok** connects through TikTok API for Business and requests
`user.info.basic`, `user.info.username`, `user.info.profile`, `user.info.stats`,
`user.account.type`, `user.insights`, `video.list`, `video.insights`,
`video.publish`, `video.upload`, `comment.list`, `comment.list.manage`, and
`biz.spark.auth`. Both personal accounts and TikTok Business Accounts can
connect. The account identity and video list scopes — the first five and
`video.list` — are required; without them the connection fails outright. The
rest are stored as granted, and a call that needs a missing one returns
`422 connection_expired` until the customer reconnects and approves it.
TikTok always shows its consent screen, even to a customer who authorized
before.

TikTok connections made before Adeli moved to TikTok API for Business show
`reconnect_required` and must be connected again. Reconnecting keeps the same
`accountId`.

**YouTube** connects one channel through its own Google sign-in, separate from
Adeli's dashboard sign-in. Adeli requests
`https://www.googleapis.com/auth/youtube.force-ssl`, which covers uploads,
comments, and moderation, and `https://www.googleapis.com/auth/yt-analytics.readonly`
for [YouTube analytics](https://www.tryadeli.com/docs/api/analytics). Google's account chooser decides
which channel is connected: your customer's own, or one of their Brand Account
channels. Reconnecting the same channel keeps the same `accountId`; connecting a
different channel on the profile replaces it. Google always shows its consent
screen. A YouTube session that cannot finish fails with one of these
`error.code` values:

| Name            | Type | Required | Description                                                                                                                  |
| --------------- | ---- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `missing_scope` |      |          | Your customer unticked the YouTube permission on Google's consent screen. Nothing is stored.                                 |
| `no_channel`    |      |          | The Google account chosen has no YouTube channel. Start again and choose the account or Brand Account that owns the channel. |

The analytics scope is optional: a connection without it keeps every other
feature, and the analytics endpoints return `422 connection_expired` until the
customer reconnects and approves it. Google revokes a grant when your customer
removes Adeli from their Google account, and after six months unused; the
account then shows `reconnect_required`.

**X** connects one account through Adeli's X app with OAuth 2.0. Adeli
requests `tweet.read`, `tweet.write`, `users.read`, `media.write`, and
`offline.access`, all required: a session where your customer withholds one
fails with `missing_scope` and stores nothing. Reconnecting the same X account
keeps the same `accountId`; connecting a different one on the profile replaces
it.

X shows every customer a warning that the app is requesting sensitive
permissions and is not affiliated with X. It appears for any app that can post,
and cannot be removed, so tell your customers to expect it.

Every X call is billed to you at X's price, including the one read of the
account's profile that connecting makes. Until the workspace has a card on
file, starting a connect returns `402 x_billing_required` before any `authUrl`
exists. That applies even within the free accounts.
See [X API pricing](https://www.tryadeli.com/docs/pricing/x).

Instagram and Facebook both accept partial consent: the connection is stored
with whatever was granted, and the calls needing a missing permission return
`403 missing_permission`.

### Bluesky

**Bluesky** connects one account with AT Protocol OAuth. Your customer signs in
on their account's own server: bsky.social for Bluesky-hosted accounts, which
is where a connect without `handle` starts. A customer who runs their own
server, or uses another provider's, must be connected with `handle`, or they
won't find their account at bsky.social.

The consent screen names Adeli and asks for broad access to the account,
because the permissions Bluesky offers today are not finer-grained than that.
Adeli uses it to publish, list, and delete posts. It does not read direct
messages. Reconnecting the same account keeps the same `accountId`; connecting
a different one on the profile replaces it. Bluesky's API is free: connecting
and posting cost nothing beyond the connected account itself.

`error.code` values beyond the common ones:

| Name             | Type | Required | Description                                                                            |
| ---------------- | ---- | -------- | -------------------------------------------------------------------------------------- |
| `invalid_handle` |      |          | The `handle` did not resolve to a Bluesky account. Starting the connect returns `400`. |
| `missing_scope`  |      |          | The grant lacked a permission Adeli needs. It was revoked, and nothing is stored.      |

A customer can revoke Adeli in the Bluesky app's settings at any time; the
account then shows `reconnect_required`.

## Disconnecting

`DELETE /api/v1/profiles/:profileId/accounts/:accountId` removes Adeli's stored
credentials and cached history. See [Accounts](https://www.tryadeli.com/docs/api/accounts/disconnect-account).
It never deletes anything on the provider side.
