> ## Documentation Index
> Fetch the complete documentation index at: https://developers.conveyour.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Conventions

> The cross-cutting rules every ConveYour endpoint follows — base URL, response envelope, errors, pagination, CSV export, and usage limits.

Every ConveYour API endpoint follows the same handful of conventions. Learn them once here and the whole [API Reference](/api-reference/contacts/list-contacts) reads predictably — no per-endpoint surprises.

<Note>
  New to the API? Start with [Authentication](/quickstart) to create a key and make your first request, then come back here for the shared behavior.
</Note>

***

## Base URL

All requests go to your organization's subdomain:

```
https://<your-subdomain>.conveyour.com/api/...
```

Replace `<your-subdomain>` with your organization's slug — the subdomain you sign in on. For example, if you sign in at `acme.conveyour.com`, your slug is `acme`. Every request carries the `x-conveyour-token` header — see [Authentication](/quickstart).

***

## Team scoping

If your org has [Teams](https://conveyour.com/help/contacts-teams-introduction) enabled, most endpoints accept a `teams` parameter that scopes the request to one or more teams. Teams control which contacts and resources are visible, so **sending the wrong scope — or none at all — changes which records you get back.**

Pass `teams` as a query parameter on `GET` requests:

```bash theme={null}
# Single team
curl "https://<your-subdomain>.conveyour.com/api/contacts?teams=TEAM_ID" \
  -H "x-conveyour-token: YOUR_TOKEN"

# Multiple teams — repeat the key
curl "https://<your-subdomain>.conveyour.com/api/contacts?teams[]=TEAM_ID_1&teams[]=TEAM_ID_2" \
  -H "x-conveyour-token: YOUR_TOKEN"
```

On requests that send a JSON body, you can include `teams` in the body instead:

```bash theme={null}
curl -X POST "https://<your-subdomain>.conveyour.com/api/contacts" \
  -H "x-conveyour-token: YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "teams": ["TEAM_ID"]
  }'
```

<ParamField query="teams" type="string | array">
  One or more team IDs. Accepts a single value or an array. Read from the query string first, then from the JSON body.
</ParamField>

<Warning>
  **Invalid team IDs are silently ignored.** Every value must be a valid 24-character ObjectId — anything else is dropped without an error, and the request behaves as if that team had not been sent. A mistyped team ID therefore looks like a permissions or "missing data" bug rather than a bad request. Log the `teams` values you send and verify them against [`GET /api/teams`](/api-reference/teams/list-teams).
</Warning>

<Note>
  Teams is a standard feature on ConveYour-enabled orgs. If your org is on a legacy program that does not use Teams, omit this parameter entirely. See [Teams Introduction](https://conveyour.com/help/contacts-teams-introduction) in the Help Center for how teams affect contact and resource visibility.
</Note>

***

## Response envelope

Every response — success or failure — uses the same three-field envelope:

```json theme={null}
{
  "status": "ok",
  "message": "Human-readable description",
  "data": {}
}
```

| Field | Type | Description |
| - | - | - |
| `status` | `string` | `"ok"` on success, `"failed"` on error |
| `message` | `string` | Human-readable description of the result |
| `data` | `object` or `array` | The payload — shape depends on the endpoint |

Always branch on the `status` field, not just the HTTP code.

***

## Errors

Errors use the same envelope with `status: "failed"`, a `message`, and often a `data` object carrying field-level validation details.

```json theme={null}
{
  "status": "failed",
  "message": "The email field is required",
  "data": {
    "email": "The email field is required"
  }
}
```

HTTP status codes:

| Code | Meaning |
| - | - |
| `200` | **Not necessarily success** — check `status` in the body |
| `400` | Bad request — validation failed or malformed input |
| `403` | Forbidden — key lacks permission, or wrong key type |
| `404` | Resource not found |
| `429` | Usage limit reached (see [Usage limits](#usage-limits)) |

<Warning>
  **The HTTP code is not a reliable success signal.** Error handling is inconsistent across the API: some endpoints return `400`/`404` on failure, but many return `200` with `"status": "failed"` in the body.

  **Around a third of the documented endpoints return HTTP `200` on a failed request** — 67 endpoints at the last full measurement — spanning Groups, Campaigns, Contacts, Custom Object Records, Fields, Files, Lessons, Messages, Notes, Org, Reports, Activity, Links, Snippets, Triggers and Users.

  ```js theme={null}
  // Wrong — silently treats failures as successes
  if (response.ok) { /* ... */ }

  // Right — the body is authoritative
  const body = await response.json()
  if (body.status === 'ok') { /* ... */ }
  ```
</Warning>

***

## Pagination

<Warning>
  **There is no shared pagination model.** Of the 57 list endpoints, **8 return a paginated envelope and 29 ignore `page` entirely**, returning a flat array. The rest return a single object rather than a collection.

  Do not assume `page` works. Check the endpoint's reference page.
</Warning>

### Endpoints that paginate

Three return the **full** envelope, and they honour `per_page` — but **only when you send `page`**. Omit it and you get a bare flat array with no envelope keys at all (`$page` defaults to `0`, and the envelope is built only `if ($page > 0)`):

| Endpoint | `data` keys |
| - | - |
| `GET /api/custom/records` | `count`, `page`, `pages`, `per_page`, `has_more`, `has_less`, `results` |
| `GET /api/form/records` | same |
| `GET /api/form/submissions` | same |

Four return a **reduced** envelope — no `per_page`, no `has_more`/`has_less`:

| Endpoint | `data` keys |
| - | - |
| `GET /api/contacts` | `count`, `page`, `pages`, `results` — but see the `limit` rule below |
| `GET /api/files` | `count`, `page`, `pages`, `results` — **only when `page` is sent**, otherwise a flat array |
| `GET /api/notes` | `count`, `page`, `pages`, `results` |
| `GET /api/schedule` | `count`, `page`, `pages`, `results` |

And one returns `results` with **no page metadata at all**:

| Endpoint | `data` keys |
| - | - |
| `GET /api/messages/events` | `results` only — and it requires `contact_id` or `message_id` |

```json theme={null}
{
  "status": "ok",
  "message": "38 contacts found",
  "data": {
    "page": 1,
    "pages": 4,
    "count": 38,
    "results": [{ "id": "..." }]
  }
}
```

| Field | Description |
| - | - |
| `page` | The current page |
| `pages` | Total number of pages |
| `count` | Total matching records across all pages |
| `per_page` | Page size in effect — **only** on the three full-envelope endpoints |
| `has_more` / `has_less` | Whether a next/previous page exists — **only** on the three full-envelope endpoints |
| `results` | The records for this page |

<Warning>
  **`results` does not imply pagination.** `GET /api/campaigns`, `GET /api/campaigns/default` and `GET /api/triggers` return an object containing `results` alongside `types` and `schemas` — a composite payload, not a page. There is no `page` or `count`, and sending `page` changes nothing.
</Warning>

### Everything else returns a flat array

29 list endpoints — including Tags, Teams, Snippets, Fields, Messages, Email templates, Groups, Lessons, Reports, Custom Objects, Links and Form submission collections — return the full set as an array whether or not you send `page`:

```json theme={null}
{
  "status": "ok",
  "message": "success",
  "data": [{ "id": "..." }, { "id": "..." }]
}
```

### Contacts is limit-driven, not page-driven

`GET /api/contacts` decides its shape from `limit`, and the maximum page size is **50**:

* `limit` omitted, or `limit` >= 50 -> paginated. `data` has `count`, `page`, `pages`, `results`; `message` is `"N contacts found"`.
* `limit` below 50 -> **not** paginated. `data` has only `results`; `message` is `"success"`.

Contacts always wraps its records in `data.results` — it never returns a bare array.

<Warning>
  Because `data` carries an array on some endpoints and an object on others, check the type before iterating. Code that assumes one shape will break when it moves to another endpoint.
</Warning>

***

## CSV export

Five endpoint families accept `format=csv` and return a CSV file instead of JSON. These are the only ones that implement it — sending `format=csv` anywhere else is ignored and you get the normal JSON envelope:

* Contacts — `GET /api/contacts`
* Custom Object Records — `GET /api/custom/records`
* Reports — `GET /api/reports`
* Message events — `GET /api/messages/events` (**not** `GET /api/messages`, which has no CSV support)
* Shared lesson items — `GET /api/lessons/{id}/shared/{item_uuid}` (**not** `GET /api/lessons`)

One endpoint returns CSV whether or not you ask: `GET /metrics/export` always responds with a CSV file and never a JSON envelope.

```bash theme={null}
curl "https://<your-subdomain>.conveyour.com/api/custom/records?obj_id=OBJECT_ID&format=csv" \
  -H "x-conveyour-token: YOUR_TOKEN" \
  -o records.csv
```

The response is a file download (`Content-Disposition: attachment`), not the JSON envelope. CSV export is unpaginated — it returns the full matching set.

***

## Usage limits

Some write endpoints are metered against your plan's quotas (contacts, teams, and similar). When a quota is exceeded, the request returns **`429`** with an explanatory message:

```json theme={null}
{
  "status": "failed",
  "message": "You have reached your contacts limit.",
  "data": {}
}
```

<Note>
  This is a **plan usage quota**, not a per-second rate limit — retrying immediately won't help. Upgrade the plan or free up capacity (e.g. archive contacts), then retry.
</Note>

`429` has **three distinct causes**, and they need different handling. The response tells you which one you hit:

| Cause | Where it applies | Envelope | What to do |
| - | - | - | - |
| **Plan quota** | `POST` to `/api/teams`, `/api/campaigns`, `/api/users`, `/api/users/restore/{id}`, `/api/custom/objects`, `/api/custom/records`, `/api/form/records`, `/api/form/submissions`, `/api/form/submission_collections` | `"status": "failed"`, `data` holds the usage overview | Upgrade or free capacity — retrying won't help |
| **Per-IP rate limit** | `POST /api/contacts` and `POST /api/users` — **20 requests per minute per IP** | `"status": "Failed"` (capital F), **no `data` key** | Wait and retry; back off |
| **Batch size** | `POST /api/custom/records/add` — more than **100 records** per call | `"status": "failed"` | Split the payload into chunks of 100 |

<Warning>
  The per-IP limit is applied **before** the request is processed, which is why its envelope is the odd one out — `Failed` with a capital F and no `data` key. If you branch on `status === 'failed'` with a lowercase comparison, this response will slip through your error handling.
</Warning>

Message sends are metered separately: `POST /api/messages` returns `429` against the `sms` or `emails` quota. That check happens after validation rather than up front, because the limit depends on how many contacts the message targets.

***

## Dates and IDs

* **IDs are not all the same type.** Most resources use an internal ID: a 24-character hex string, e.g. `64a1b2c3d4e5f6a7b8c9d0e1` — Tags, Snippets, Groups, Lessons, Campaigns, Triggers, Custom Objects, Custom Object Records and Form submission collections all do.

  These do **not**:

  | Resource | `id` type | Example |
  | - | - | - |
  | Links | **integer** | `1` |
  | Notes | **integer** | `55` |
  | Fields | **integer** | `12` |
  | Reports | **string key** | `campaignsCampaignPerformance` |

  Type your client's ID fields accordingly — code that assumes a 24-character string will break on these, and code that assumes an integer will break everywhere else.

* Some resources also expose a shorter **public ID (`pid`)** used for portal-facing and shareable links — see the public Exports and shared Reports endpoints, which are reachable with the `pid` alone and no token.

* **Timestamps** are typically Unix seconds (integers) on stored records; some endpoints return ISO-8601 strings. The reference page notes the format per field.

***

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/quickstart">
    Create a key and make your first request.
  </Card>

  <Card title="Liquid Syntax" icon="code" href="/guides/liquid-syntax">
    Personalize messages and query data inside templates.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.