---
title: "API conventions"
sidebarTitle: "Conventions"
description: "Idempotency, errors, pagination, versioning and rate limits for the Voice Engine API."
---

> **For AI agents:** the complete documentation index is at [llms.txt](https://docs.trillet.ai/llms.txt). Append `.md` to any page URL for its markdown version.

<Warning>
  **Coming soon.** The Voice Engine API is in development. This page describes the planned design; details may change before release.
</Warning>

- **Base URL:** `https://api.trillet.ai/partner/v1`
- **Format:** JSON request and response bodies, UTF-8, timestamps in ISO 8601 UTC.
- **IDs:** Trillet IDs are opaque strings. Store them as strings.

## Idempotency

Networks fail. Send an `Idempotency-Key` header on every request that creates something, so a retry never creates it twice. Tenant creation requires one.

```bash
curl -X POST https://api.trillet.ai/partner/v1/tenants \
  -H "Authorization: Bearer tpk_live_..." \
  -H "Idempotency-Key: create-tenant-cust_8842" \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "cust_8842", "name": "Harbour Dental" }'
```

| Situation | Result |
|---|---|
| First request with a key | Runs normally. The response is stored for 24 hours. |
| Retry with the same key and the same body | The stored response is returned with `Idempotent-Replayed: true`. Nothing runs twice. |
| Same key, different body | `422` `idempotency_key_reused` |
| Same key while the first request is still running | `409` `idempotency_key_in_use`. Retry after `Retry-After`. |
| The first request failed with a `5xx` | Not stored. A retry runs again. |

Keys are 1 to 255 printable characters and are scoped to your partner account. A UUID, or your own operation ID, works well.

## Errors

Every error has the same shape:

```json
{
  "error": {
    "type": "invalid_request",
    "code": "tenant_deleted_restorable",
    "message": "A deleted tenant with this externalId can still be restored.",
    "param": "externalId",
    "requestId": "req_7Qe2..."
  }
}
```

Handle errors by `code`, not by `message`. Include `requestId` when you contact support.

| Status | Meaning |
|---|---|
| `400` | The request is malformed or a required field is missing |
| `401` | Authentication failed |
| `403` | Not allowed for this key |
| `404` | Not found, or not yours |
| `409` | Conflict, for example a duplicate `externalId` or a request still in progress |
| `422` | The request is well formed but cannot be applied |
| `429` | Rate limited |
| `5xx` | A problem on our side. Safe to retry with the same `Idempotency-Key`. |

## Pagination

List endpoints return a page and a cursor:

```json
{
  "data": [ { "id": "ten_1" }, { "id": "ten_2" } ],
  "hasMore": true,
  "nextCursor": "ten_2"
}
```

Pass `limit` (1 to 100, default 20) and `starting_after=<nextCursor>` to get the next page.

## Versioning

The major version is in the path (`/partner/v1`). Within a version we only add things: new endpoints, new optional fields, new event types. Your code should ignore fields and event types it does not recognise.

Breaking changes ship as a new major version, with notice and an overlap period. Webhook payloads carry an `apiVersion` field.

## Rate limits

Limits apply per key. The default is 100 requests per second in bursts and 6,000 per minute. A `429` response includes `Retry-After`. Contact us if your integration needs more.
