# Core concepts

> Profiles, accounts, the partial-response contract, and the limits that apply to every read.

Canonical: <https://www.tryadeli.com/docs/concepts>

Four ideas explain most of the API's behavior: profiles, accounts, the
partial-response contract, and the limits on aggregate reads.

## Profiles

A profile is a client boundary — one of your end customers or brands. It owns
the provider connections made on its behalf. Profiles live in your
**organization**, the workspace your team shares, and an API key belongs to the
organization, so one key can act on every profile in it.

**Profile**

| Name         | Type             | Required | Description                                                                                                             |
| ------------ | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `id`         | `uuid`           |          | Adeli's identifier. Use it in every path that takes a profileId.                                                        |
| `name`       | `string`         |          | Display name, unique within your organization. 1–200 characters.                                                        |
| `externalId` | `string \| null` |          | Your own identifier for this customer, so you do not have to store a mapping. Unique within your organization when set. |
| `metadata`   | `object \| null` |          | Arbitrary JSON you control, up to 16 KiB encoded as UTF-8.                                                              |
| `isDefault`  | `boolean`        |          | Whether this is the profile new dashboard sessions start on.                                                            |

Profiles are created in the dashboard or with
[`POST /api/v1/profiles`](https://www.tryadeli.com/docs/api/profiles), and listed with
`GET /api/v1/profiles`. Deleting one cascades: its connected accounts and cached
history go with it. API keys belong to the organization and are not affected.

## Accounts

An account is one provider connection inside a profile — an Instagram
professional account, a Facebook Page, a TikTok account, or a YouTube channel.
A profile holds at most one of each.

Every endpoint that acts on a provider takes `accountId`, which is **Adeli's**
UUID. The provider's own identifier comes back as `providerId` and is
informational; passing it where an `accountId` is expected returns
`400 invalid_request`.

**Account**

| Name                | Type                                                 | Required | Description                                                                                                    |
| ------------------- | ---------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `accountId`         | `uuid`                                               |          | The identifier to use in requests. `id` is the same value.                                                     |
| `platform`          | `"instagram" \| "tiktok" \| "facebook" \| "youtube"` |          | Which provider this connection belongs to.                                                                     |
| `providerId`        | `string`                                             |          | The provider's identifier — an Instagram user id, TikTok's app-scoped open\_id, or a YouTube channel id.       |
| `displayName`       | `string`                                             |          | Human-readable name, such as "@adeli".                                                                         |
| `displayIdentifier` | `string`                                             |          | The handle or open\_id, without decoration.                                                                    |
| `connectionStatus`  | `"connected" \| "expired" \| "reconnect_required"`   |          | Whether Adeli can still act on this account. Anything but connected means the customer has to authorize again. |

Instagram long-lived tokens last roughly 60 days. Adeli refreshes them, and
`POST /api/v1/profiles/:profileId/accounts/refresh` forces the attempt, but a
connection that has lapsed needs the customer to go back through
[the connect flow](https://www.tryadeli.com/docs/api/connect).

## Scoping a request

Every request names the profile it acts on, and nothing defaults to one.

- **Pass `profileId`** in the path, the query string, or the body.
- **Or pass `accountId`.** An account belongs to exactly one profile, so it names
  that profile and `profileId` can be left out. If you pass both, they must
  agree, or you get `404 account_not_found`.
- **Pass neither** and you get `400 profile_required`.
- **Pass a profile or account in another organization** and you get
  `404 profile_not_found` or `404 account_not_found`, whether or not it exists.

Only `GET` and `POST /api/v1/profiles` and `GET /api/v1/usage/x` act on the
whole organization and take no profile.

Add `accountId` to narrow a read to one connection, and `platform` to narrow it
to one provider.

## Partial responses

Aggregate reads — accounts, posts, messages — can touch more than one provider
in a single request. One provider failing does not throw away the data from the
others.

| HTTP          | `status`   | Meaning                                                                                                                                                                                   |
| ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`         | `complete` | Every attempted provider succeeded. `errors` is empty.                                                                                                                                    |
| `207`         | `partial`  | At least one succeeded and at least one failed. Successful records are still in the response; `errors` says which account failed and why.                                                 |
| `404` / `422` | —          | Every attempted provider failed for the same reason, and that reason has a meaningful status: `account_not_found` is `404`, `connection_expired` and `provider_not_configured` are `422`. |
| `502`         | —          | Every attempted provider failed for mixed or upstream reasons.                                                                                                                            |

A partial response looks like this:

```json
{
  "status": "partial",
  "messages": [],
  "errors": [
    {
      "platform": "instagram",
      "accountId": "00000000-0000-0000-0000-000000000000",
      "code": "provider_error",
      "message": "The provider request failed"
    }
  ]
}
```

> **Warning: Treat 207 as success with caveats**
>
> A client that only checks `response.ok` will silently accept `207` and act on
> an incomplete list. Check `status` and inspect `errors` before you treat an
> aggregate read as the whole picture.

Provider error bodies are never passed through. An upstream failure becomes a
sanitized `provider_error` so that provider credentials and internal URLs cannot
leak into your logs.

## Limits

**What applies to every request**

| Name              | Type               | Required | Description                                                                                                                                                                                                                                                                                                                               |
| ----------------- | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Pagination`      | `none`             |          | Adeli follows each provider's cursors server-side and returns one flat array. There are no cursors in the public API. Filter by `platform` and `accountId` to keep responses small.                                                                                                                                                       |
| `Result cap`      | `10,000`           |          | The maximum number of records in one collection response. Exceeding it truncates the array and adds a `result_limit` entry to `errors`, which makes the response `207`.                                                                                                                                                                   |
| `Rate limiting`   | `none`             |          | Not implemented in v1. Do not rely on its absence — be reasonable, and expect limits to arrive before general availability.                                                                                                                                                                                                               |
| `Caching`         | `none`             |          | Every response carries `cache-control: no-store`. Reads hit the provider.                                                                                                                                                                                                                                                                 |
| `Instagram image` | `8 MiB`            |          | Decoded size for a single image or each carousel entry. JPEG, PNG, and WebP are accepted and normalized to JPEG.                                                                                                                                                                                                                          |
| `TikTok video`    | `64 MiB / 1 GB`    |          | 64 MiB decoded for base64 JSON bodies; 1 GB for multipart uploads and public URL pulls.                                                                                                                                                                                                                                                   |
| `TikTok photo`    | `20 MB`            |          | Per photo, up to 35 photos, normalized to JPEG at 1080×1920.                                                                                                                                                                                                                                                                              |
| `YouTube video`   | `1 GB / 4 GiB`     |          | 1 GB for a public `video.url` pull; 4 GiB for a multipart upload to `POST /api/v1/posts/youtube`. Base64 JSON bodies are capped by the request size.                                                                                                                                                                                      |
| `YouTube quota`   | `10,000 units/day` |          | YouTube's daily quota belongs to Adeli's Google Cloud project and is shared by every connected channel. Reads cost 1 unit, every comment write and video edit 50, and uploads draw on a separate allowance of 100 a day. When the day's quota is spent, YouTube calls return `429 quota_exhausted` with the reset time, midnight Pacific. |

## Idempotency and retries

There is no idempotency key on the public API. `POST /api/v1/posts` and
`POST /api/v1/messages` are not safe to retry blindly — a retry after a timeout
may publish twice. If a publish times out, list posts or query the TikTok or
YouTube status endpoint to find out what actually happened before trying again.

Reads are safe to retry.

## What is not here

- **Post editing and deletion.** `PUT` and `DELETE` on posts are deliberately not part of v1.
- **Outbound webhooks.** Adeli does not call your server. Poll instead.
- **WhatsApp free-form replies and connecting a number.** The API sends WhatsApp templates; see [Messages](https://www.tryadeli.com/docs/api/messages).
- **Comments.** Not exposed through the API.
- **Scheduling.** `scheduled_date`, `add_to_queue`, and `async_upload` are rejected with `422 unsupported_feature`.
