# Errors

> The error envelope, every status code the API returns, and what each error code means.

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

Every failure returns JSON in the same shape, whatever went wrong.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "accountId must be a valid UUID",
    "details": [{ "path": ["accountId"], "message": "Invalid uuid" }]
  }
}
```

`code` is stable and safe to branch on. `message` is written for a human reading
a log and may change. `details` is present only when there is something more to
say — validation issues, or the per-account `errors` array from an aggregate
read.

## Status codes

| Status | When                                                                                                                                                                                                                                                                                                                    |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | Success, or an aggregate read where every provider succeeded.                                                                                                                                                                                                                                                           |
| `201`  | A resource was created — an Instagram post, a message, a connection session.                                                                                                                                                                                                                                            |
| `202`  | A TikTok or YouTube publish, or a Facebook video, was accepted. It is not published yet; [poll for status](https://www.tryadeli.com/docs/api/posts).                                                                                                                                                                    |
| `207`  | An aggregate read where some providers succeeded and some failed.                                                                                                                                                                                                                                                       |
| `400`  | The request is malformed: bad JSON, a failed schema check, a non-UUID identifier, or no profile named (`profile_required`).                                                                                                                                                                                             |
| `402`  | Billing is needed first: `billing_required` when a connect would pass the workspace's free accounts with no card on file (see [Billing](https://www.tryadeli.com/docs/billing#in-the-api)), or `x_billing_required` when X is used with no card on file (see [X API pricing](https://www.tryadeli.com/docs/pricing/x)). |
| `403`  | A connection has not granted a permission the call needs: `missing_permission`, or `ads_permission_required` for [Ads](https://www.tryadeli.com/docs/api/ads).                                                                                                                                                          |
| `404`  | The resource does not exist, **or** it exists but your key cannot reach it.                                                                                                                                                                                                                                             |
| `413`  | The request body is too large.                                                                                                                                                                                                                                                                                          |
| `415`  | `Content-Type` is neither `application/json` nor `multipart/form-data`.                                                                                                                                                                                                                                                 |
| `422`  | The request is well-formed but the operation is not possible: an unsupported platform, an expired connection, invalid media, a deferred feature.                                                                                                                                                                        |
| `429`  | The provider is throttling this account or app (TikTok), the account hit TikTok's daily posting limit, or YouTube's shared daily quota is spent. Retry later.                                                                                                                                                           |
| `502`  | Every attempted provider failed, or a database read failed. Provider details are sanitized away.                                                                                                                                                                                                                        |
| `503`  | X is unavailable to Adeli for a while: `temporarily_unavailable`. Retry later.                                                                                                                                                                                                                                          |

> **Note: 404 covers authorization, on purpose**
>
> Supplying a `profileId` or `accountId` that belongs to another organization
> returns `404`, not `403`. The API will not confirm that a resource exists
> outside your key's organization. A `profileId` and `accountId` that disagree
> also return `404 account_not_found`, the same answer as an unknown account.

## Error codes

### Authentication and scope

| Name                | Type  | Required | Description                                                                                                                                                                 |
| ------------------- | ----- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unauthorized`      | `401` |          | The Authorization header is missing, malformed, or names a key that has been deleted.                                                                                       |
| `profile_required`  | `400` |          | The request named no profile. Pass `profileId`, or an `accountId` that belongs to a profile. Only `GET` and `POST /api/v1/profiles` and `GET /api/v1/usage/x` need neither. |
| `profile_not_found` | `404` |          | The `profileId` does not exist, or it belongs to another organization.                                                                                                      |

### Billing

| Name               | Type  | Required | Description                                                                                                                                                                                                                       |
| ------------------ | ----- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `billing_required` | `402` |          | Starting a connection would take the workspace past its three free accounts, and no card is on file. `details.billingUrl` is where the workspace owner adds one. See [Billing](https://www.tryadeli.com/docs/billing#in-the-api). |

### Request validation

| Name                     | Type        | Required | Description                                                                                                        |
| ------------------------ | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
| `invalid_request`        | `400 / 413` |          | Malformed JSON, a schema failure, a non-UUID identifier, or a body over the size limit. Check `details`.           |
| `invalid_profile`        | `400`       |          | Profile fields failed their own checks — for example `metadata` over 16 KiB.                                       |
| `invalid_redirect_uri`   | `400`       |          | The connect `redirectUrl` is not a valid URL, its origin is not allowlisted, or it is not HTTPS outside localhost. |
| `unsupported_media_type` | `415`       |          | `Content-Type` must be `application/json` or `multipart/form-data`.                                                |

### Conflicts

| Name                           | Type  | Required | Description                                                                           |
| ------------------------------ | ----- | -------- | ------------------------------------------------------------------------------------- |
| `profile_name_conflict`        | `409` |          | Another profile already uses this name. `details.existingProfileId` names it.         |
| `profile_external_id_conflict` | `409` |          | Another profile already uses this `externalId`. `details.existingProfileId` names it. |
| `idempotency_conflict`         | `409` |          | The `Idempotency-Key` was already used with a different request body.                 |

### Accounts and connections

| Name                           | Type  | Required | Description                                                                                                                                                                                                                          |
| ------------------------------ | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `account_not_found`            | `404` |          | No connected account with that id in your key's organization, or it is not on the `profileId` you also passed.                                                                                                                       |
| `connection_session_not_found` | `404` |          | No connection session with that id for this profile.                                                                                                                                                                                 |
| `connection_expired`           | `422` |          | The stored provider token has lapsed, or the required scope was never granted. The customer has to reconnect.                                                                                                                        |
| `connection_unavailable`       | `—`   |          | Appears inside an aggregate errors array when one account's credentials could not be used for that read.                                                                                                                             |
| `provider_not_configured`      | `422` |          | This Adeli deployment has no credentials for that provider.                                                                                                                                                                          |
| `provider_not_supported`       | `422` |          | The provider cannot do this. WhatsApp and Google Business Profile connect sessions, and TikTok and YouTube messaging, land here.                                                                                                     |
| `missing_scope`                | `—`   |          | A YouTube, X, or Bluesky connect session's `error.code`: the customer withheld a permission on the provider's consent screen.                                                                                                        |
| `invalid_handle`               | `400` |          | A Bluesky `handle` did not resolve to an account, when starting a connect or as a connect session's `error.code`.                                                                                                                    |
| `x_billing_required`           | `402` |          | X is billed per call and needs a card on file, even within your free accounts. Connecting, posting, deleting, and listing X posts all return it until a card is added. See [X API pricing](https://www.tryadeli.com/docs/pricing/x). |
| `reconnect_required`           | `409` |          | Turning on an X capability needs a permission the connection was not granted. `details.missingScopes` lists them.                                                                                                                    |
| `no_channel`                   | `—`   |          | A YouTube connect session's `error.code`: the Google account chosen has no YouTube channel.                                                                                                                                          |

### Publishing

| Name                        | Type  | Required | Description                                                                                                                                                                    |
| --------------------------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `unsupported_platform`      | `422` |          | The endpoint does not serve that platform — listing posts covers Instagram, Facebook, TikTok, YouTube, X, and Bluesky, and messages are Instagram, Facebook, or WhatsApp only. |
| `unsupported_feature`       | `422` |          | Scheduling, queueing, and background uploads are deferred. `scheduled_date`, `add_to_queue`, and `async_upload` are rejected.                                                  |
| `invalid_image`             | `422` |          | The image is not decodable, not a supported format, or over the size limit.                                                                                                    |
| `invalid_video`             | `422` |          | The video is not a usable MP4, is over the size limit, or exceeds the creator's maximum duration.                                                                              |
| `privacy_level_unsupported` | `422` |          | A TikTok video asked for a `privacy_level` other than `PUBLIC_TO_EVERYONE`. TikTok videos publish publicly; send `post_mode: "MEDIA_UPLOAD"` for a draft instead.              |
| `invalid_post_settings`     | `422` |          | The requested privacy, comment, duet, or stitch setting is not available to this creator, or branded content was requested without public visibility.                          |
| `media_url_unverified`      | `422` |          | TikTok has not verified this deployment's media URL prefix, which it must before any TikTok post works.                                                                        |
| `upload_limit_exceeded`     | `422` |          | The YouTube channel has reached YouTube's own upload limit. Try again later.                                                                                                   |
| `provider_rejected`         | `422` |          | The provider refused the post's content or metadata, such as a YouTube category\_id that does not exist.                                                                       |
| `duplicate_post`            | `409` |          | X refused the post because the account published the same text recently.                                                                                                       |
| `invalid_media`             | `422` |          | An X media item is not valid base64, does not match its content\_type, is over its size limit, or X could not process it.                                                      |

### Comments

| Name                | Type  | Required | Description                                                                                                  |
| ------------------- | ----- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `post_not_found`    | `404` |          | The `postId` is not one of this account's posts.                                                             |
| `comment_not_owned` | `422` |          | TikTok deletes only comments the account itself wrote. Hide other people's comments instead.                 |
| `not_supported`     | `422` |          | The platform cannot do this: liking a comment outside TikTok, or `cursor` paging outside TikTok and YouTube. |

### Upstream

| Name                      | Type  | Required | Description                                                                                                                                              |
| ------------------------- | ----- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider_error`          | `502` |          | The provider request failed. The upstream body is deliberately not passed through.                                                                       |
| `rate_limited`            | `429` |          | TikTok is throttling this account (40 requests a minute per endpoint) or the app, or YouTube is throttling per-second. Retry with backoff.               |
| `quota_exhausted`         | `429` |          | YouTube's daily API quota, shared by every Adeli customer, is used up. The message says when it resets, at midnight Pacific; retrying before then fails. |
| `provider_unavailable`    | `502` |          | Every attempted provider failed for mixed reasons. `details` carries the per-account errors.                                                             |
| `temporarily_unavailable` | `503` |          | X is unavailable to Adeli for a while. Retry later.                                                                                                      |
| `database_error`          | `502` |          | Adeli could not read or write its own store. Safe to retry.                                                                                              |
| `result_limit`            | `—`   |          | Appears inside an aggregate `errors` array when a collection was truncated at 10,000 records, which makes the response `207`.                            |

## Handling errors

Branch on `code`, not on `message`, and treat `207` as its own case.

**JavaScript**

```javascript
const response = await fetch(`${BASE}/accounts?profileId=${profileId}`, { headers });
const body = await response.json();

if (!response.ok) {
  switch (body.error.code) {
    case "unauthorized":
      throw new Error("Adeli key is invalid or revoked");
    case "connection_expired":
      return promptCustomerToReconnect();
    default:
      throw new Error(`${body.error.code}: ${body.error.message}`);
  }
}

// 200 and 207 both land here.
if (body.status === "partial") {
  console.warn("Incomplete result", body.errors);
}

return body.accounts;
```

**Python**

```python
response = requests.get(f"{BASE}/accounts", headers=HEADERS, params={"profileId": profile_id})
body = response.json()

if not response.ok:
    code = body["error"]["code"]
    if code == "unauthorized":
        raise RuntimeError("Adeli key is invalid or revoked")
    if code == "connection_expired":
        return prompt_customer_to_reconnect()
    raise RuntimeError(f"{code}: {body['error']['message']}")

# 200 and 207 both land here.
if body["status"] == "partial":
    logging.warning("Incomplete result: %s", body["errors"])

return body["accounts"]
```

Retry `502`, `429`, and `database_error` with backoff. Do not retry `4xx` — the request
will fail the same way — and be careful retrying a publish that timed out, since
[there is no idempotency key](https://www.tryadeli.com/docs/concepts#idempotency-and-retries).
