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

# Envelopes and errors

> One response wrapper for every success, one error format with a closed 16-code taxonomy.

Every endpoint in every service answers with one of two envelopes. You write the parsing code once.

## The success envelope

A success body always carries `success: true`, the `operation` that names the payload shape, the payload itself, and a `meta` block.

| `operation` | Payload | Returned by |
| - | - | - |
| `search` | `items[]`, `counts`, `pagination`, `applied_filters`, `includes` | `POST /api/{entity}/search` |
| `get` | `item`, `included` | `GET /api/{entity}/{sid}` |
| `create` | `item`, plus flags such as `already_exists` where relevant | `POST /api/{entity}` |
| `update` | `item`, `changed_fields` | `PATCH /api/{entity}/{sid}` |
| `delete` | deletion report | `DELETE /api/{entity}/{sid}` |
| `metrics` | period-bound aggregates, split by the `group_by` you ask for | `POST /api/{entity}/metrics` |
| `action` | `action` name plus a verb-specific `result` | verb endpoints like `POST /api/workflows/{sid}/pause` |

A `search` response in full:

```json theme={null}
{
  "success": true,
  "operation": "search",
  "items": [
    {
      "item": { "sid": "wf_wf_Jk2mP9nR4qW1", "status": "running" },
      "included": {}
    }
  ],
  "counts": {
    "total_count": 12,
    "groups": { "status": { "running": 9, "paused": 3 } }
  },
  "pagination": {
    "next_cursor": "eyJpZCI6MTAwfQ==",
    "has_more": true,
    "total_count": 12
  },
  "applied_filters": { "status": { "in": ["running", "paused"] } },
  "includes": [],
  "meta": {
    "trace_id": "0198f2ab-7c11-7e32-9a41-d2b64f2a91c3",
    "span_id": "9a41d2b64f2a91c3",
    "timestamp": "2026-11-01T12:00:00Z",
    "duration_ms": 42,
    "team_sid": "ts_tm_Hx7kQ3mN2pL4",
    "actor_type": "api_key"
  }
}
```

Points worth knowing:

* **`items[]` splits `item` from `included`.** The `item` is always the same typed shape; relations you request via `include` arrive next to it under `included`, never mixed into it.
* **`counts` covers the full filter scope, not the page.** With 50 rows shown out of 1,000 matching, `counts.total_count` is 1000, and `counts.groups` breaks the same scope down by the fields the entity groups on.
* **`applied_filters` echoes what the server actually applied**, so a client (or an AI agent) can confirm the query it asked for is the query that ran.
* **`meta.trace_id` is the support handle.** It is a UUID v7, identical to the `X-Trace-Id` response header. Send your own `X-Trace-Id` (a UUID or a 32-hex trace id) and the server keeps it. Quote it when reporting an issue.

## The error envelope

Every failure, from any service, is the same shape:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "validation_failed",
    "message": "page_size must be between 0 and 500",
    "recoverable": true,
    "suggestion": "Lower page_size and page with the cursor instead",
    "field_errors": {
      "page_size": [
        { "rule": "max", "message": "page_size must be between 0 and 500" }
      ]
    }
  },
  "meta": {
    "trace_id": "01a1956b-536d-72cf-afc6-d09de54bf361"
  }
}
```

* `code` is always one of the 16 values below, never an arbitrary string.
* `recoverable` says whether retrying with corrected input can succeed (`true`) or the state itself is the problem (`false`).
* `suggestion` is a sentence written for a person or an LLM to act on.
* `field_errors` appears only on `validation_failed`, keyed by field path. Each entry's `rule` is the machine reason and its `message` the sentence for it.
* On `conflict`, `rate_limited`, `payment_required` and `service_unavailable` the machine reason is `error.context.reason`, and the ids, dates and limits it refers to sit next to it in `error.context` (a `retry_after`, the `status` a state machine found, the `sid` of the row that already exists).
* `delete_blocked` errors additionally carry `error.blockers[]`: each blocker names its `type`, a `hard` or `soft` severity, and the `resolution` that clears it.

## The 16 error codes

| Code | HTTP | Meaning |
| - | - | - |
| `validation_failed` | 422 | Invalid input; details in `field_errors` |
| `nothing_to_update` | 422 | PATCH with no real changes |
| `not_found` | 404 | The sid does not exist in this workspace's scope |
| `relation_not_found` | 422 | A referenced entity (for example a `*_sid` field) does not exist |
| `invalid_transition` | 409 | The state machine forbids this move |
| `limit_exceeded` | 429 | A business limit was hit (sending budgets, per-thread caps, plan sizes) |
| `payment_required` | 402 | The subscription does not include this, or the workspace is on hold |
| `duplicate_rejected` | 409 | An entity with the same natural key already exists |
| `conflict` | 409 | The request conflicts with current state; editing fields will not fix it |
| `delete_blocked` | 409 | Blockers exist; see `error.blockers[]` |
| `unauthorized` | 401 | Missing or invalid token |
| `forbidden` | 403 | No rights on this workspace or resource |
| `rate_limited` | 429 | Throttled; wait for `context.retry_after` and retry |
| `not_implemented` | 501 | Contract is final, capability not shipped; do not retry |
| `internal_error` | 500 | A bug on our side; report the `trace_id` |
| `service_unavailable` | 503 | A dependency did not answer; retry with backoff |

A practical mapping: retry `rate_limited` and `service_unavailable` with backoff, fix and resend `validation_failed` and `relation_not_found`, treat the 409 family as "re-read the current state first", and never auto-retry `not_implemented` or `internal_error`.

## A refusal over MCP is a tool result

On the [MCP server](https://docs.getsales.io/mcp/overview.md) the same envelope arrives inside the tool result: HTTP 200, `isError: true`, the envelope in `structuredContent` and the same facts in the text block, ending with the trace id. A client reads `isError` and `error.code`, never the text. The HTTP status speaks only before a tool runs: `401` with the OAuth challenge, `403` on a mount the token's actor may not use, `404` for an unknown mount, `413` for an over-long batch.


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