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

# Handle API errors

> Read the JSON error body, know which statuses to retry, and recover from each one without guessing.

Every error the Salesfinity API returns is JSON. There is no HTML error page, no plain-text
fallback, and no endpoint that answers a failure differently — a client can parse a failure the
same way on every route and every status code.

The one exception is a request that never reaches the API, for example when the network edge in
front of it cannot connect. That answer comes from the edge and may not be JSON. Treat a 5xx
whose body does not parse as JSON as retryable, the same as any other 5xx.

## The error envelope

```json theme={"system"}
{
  "message": "Forbidden resource",
  "error": "Forbidden",
  "statusCode": 403,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

| Field | Type | Description |
| - | - | - |
| `message` | string or string array | Human-readable description of what went wrong. Becomes an **array of strings** when request validation fails, with one entry per field that did not validate. |
| `error` | string | The HTTP reason phrase for the status, for example `Forbidden`, `Not Found`, `Bad Request`. Stable across releases — branch on this. One exception: a Sequencer write acting as a member with no active plan or seat gets `NO_ACTIVE_PLAN` here (see [403](#403-forbidden)). |
| `statusCode` | integer | The HTTP status code, repeated in the body so it survives proxies and transports that drop it. |
| `request_id` | string | Identifies the request in Salesfinity's logs. The same value as the `X-Request-Id` response header. Include it when you contact support. |

<Note>
  Branch on `error` or `statusCode`, never on `message`. Message text is written for humans and may
  be reworded; `error` and `statusCode` are contractual.
</Note>

### Request IDs

Every response, successful or not, carries an `X-Request-Id` header, and every error body repeats
it as `request_id`. To follow a request through your own logs and ours, send your own
`X-Request-Id`: 1 to 128 characters of letters, digits, `.`, `_`, `:` and `-`. Salesfinity uses it
as the request's ID. If you send none, or one that does not fit that format, Salesfinity generates
a UUID.

## The order checks run in

A request is checked in this order, and the first check that fails decides the response. When a
request could fail in more than one way, this tells you which error you will see.

1. **The body is read.** A body that is not valid JSON gets **400**; one over the size limit gets
   **413**.
2. **The route is matched.** A method and path that match no route get **404**
   `Cannot <METHOD> <path>`.
3. **The API key is checked.** A missing or unknown key gets **403**.
4. **The rate limit is counted.** Over the limit gets **429**. Every request that got this far
   counts, whatever happens next.
5. **The `Idempotency-Key` is claimed**, on Sequencer writes that send one. A reused or busy key
   gets **409**.
6. **Parameters and body are validated.** Anything that does not match the schema gets **400**
   with one `message` entry per problem. `GET /v1/call-log`, `GET /v1/scored-calls`,
   `GET /v1/contact-lists/csv` and `GET /v1/contact-lists/csv/{id}` are the exception for unknown
   query parameters: they drop one rather than refuse it.
7. **The operation runs.** It can still answer **400**, **402**, **403**, **404**, **409**,
   **422** or a **5xx**, as its reference page says.

## Status codes

### 400 Bad Request

The request is malformed. Most often a parameter or body field failed validation, and `message` is
an array with one entry per invalid field. A field the operation does not define is a validation
failure too, except in the query of the four list endpoints named in
[step 6 above](#the-order-checks-run-in), which ignore what they do not recognize.

```json theme={"system"}
{
  "message": [
    "linkedin_url must be a linkedin.com/in/<username> URL",
    "type must be one of the following values: work, personal",
    "property phone should not exist"
  ],
  "error": "Bad Request",
  "statusCode": 400,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

`message` is a single string when the request fails before validation or in the operation itself:
a body that is not valid JSON, an ID in the path that is not a valid ID, or a rule such as a
`user_id` that is not on the team. Not every route checks the ID first; the ones that do not are
listed under [500](#500-internal-server-error).

```json theme={"system"}
{
  "message": "Invalid ObjectId format for parameter: id",
  "error": "Bad Request",
  "statusCode": 400,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** reading `message` and correcting what it names. Check the request against the
schema shown on the operation's reference page. Retrying an identical request will fail
identically.

### 402 Payment Required

The team has run out of enrichment credits. Only the requests that start an enrichment return
this; reading a result or the balance never does.

```json theme={"system"}
{
  "message": "Insufficient enrichment credits. Please top up to continue.",
  "error": "Payment Required",
  "statusCode": 402,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

These are the team's enrichment credits, which the API and the MCP server spend. They are
separate from the SmartEnrich credits members spend in the dialer, and they cannot be bought in the
dashboard.

**Recover by** asking Salesfinity to add team enrichment credits, then retrying. Call
[Get enrichment credit balance](/api-reference/endpoint/get-enrichment-credits) before a bulk run
to avoid hitting this mid-batch.

### 403 Forbidden

The `x-api-key` header is missing, malformed, revoked, or belongs to a different team.

```json theme={"system"}
{
  "message": "Forbidden resource",
  "error": "Forbidden",
  "statusCode": 403,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

<Warning>
  Salesfinity returns **403 for authentication failures, not 401**. A client written around the
  usual "re-authenticate on 401" convention will never trigger its auth-recovery path against this
  API. Handle 403 explicitly.
</Warning>

**Recover by** checking that the `x-api-key` header is present and spelled correctly, that the key
has not been revoked in **Settings → Connections & API**, and that it belongs to the team whose
data you are requesting.

A 403 with any `message` other than `Forbidden resource` is not about the key. It means the team
member you are acting as, the `user_id`, may not do this. For example, a Sequencer write acting as
an inactive member gets `The acting user is not an active member of this team`, and editing
someone else's note gets `Only the author can edit this note`. Act as a member who is allowed to.

A Sequencer write acting as a member who has no active plan or seat is refused before anything
else is checked, and its `error` is `NO_ACTIVE_PLAN` instead of `Forbidden`:

```json theme={"system"}
{
  "statusCode": 403,
  "error": "NO_ACTIVE_PLAN",
  "message": "An active plan or seat is required to use this feature. Please upgrade your plan or ask your team admin to assign you a seat.",
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

Assign that member a seat, or act as one who has one.

### 404 Not Found

Either the route does not exist, or the resource does exist but belongs to another team.

```json theme={"system"}
{
  "message": "Cannot GET /v1/unknown-route",
  "error": "Not Found",
  "statusCode": 404,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

A `message` of the form `Cannot <METHOD> <path>` means the route itself is wrong — check the
method and path against the reference. Any other message means the route was correct but the
record was not found for this team.

**Recover by** verifying the path, the HTTP method, and any ID in the URL. Because API keys are
team-scoped, the API returns 404 rather than 403 for records owned by another team; it does not
confirm that they exist.

### 409 Conflict

The request conflicts with an earlier request or with the current state of the record. The most
common cause is an `Idempotency-Key` that was already used. See
[Retrying writes safely](#retrying-writes-safely).

```json theme={"system"}
{
  "message": "This Idempotency-Key was already used with a different request body.",
  "error": "Conflict",
  "statusCode": 409,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** working out which case you hit:

* **The key was already used for a different request**, with another body or another path. Send a
  new key. Reusing a key for a different request is a bug in the client.
* **The first request with this key is still running.** The `message` is
  `A request with this Idempotency-Key is still in progress.` Wait a moment, then retry with the
  same key. Once the first request has finished, the retry returns its response.
* **The record's state rules the action out**, for example completing a `manual_email` task whose
  send is already in flight, or reordering steps while people are mid-cadence. The operation's
  reference page says when this applies. An identical retry fails the same way until the state
  changes.

### 413 Payload Too Large

The body is over the size limit: 10 MB on `/v1/contact-lists` and `/v2/sequencer`, 1 MB on every
other route. The request was not processed.

```json theme={"system"}
{
  "message": "request entity too large",
  "error": "Payload Too Large",
  "statusCode": 413,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** splitting the request. For enrolling many people, an
[enrollment run](/api-reference/endpoint/start-sequencer-enrolment-run) takes up to 5,000 contacts
in one request.

### 422 Unprocessable Entity

The Sequencer understood the request, but its rules refuse it. Examples: enabling a sequence that
has no steps or no mailbox able to send, or adding contacts to a sequence that is archived, has no
steps, or has enrollment paused. Contacts refused one by one, for example because they are
suppressed or a rule blocks them, are not a 422: the bulk operations report them in their
response.

```json theme={"system"}
{
  "message": "Sequence has no steps and cannot be enabled",
  "error": "Unprocessable Entity",
  "statusCode": 422,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** fixing what `message` names, then sending the request again. An identical retry
fails identically until then.

### 429 Too Many Requests

The team has used up a rate limit for the current minute. Limits are per team, so every API key
on the team draws on the same budget. See [Rate limits](/api-reference/rate-limits)
for the limits and the routes each one covers.

```json theme={"system"}
{
  "message": "Rate limit of 300 requests per minute exceeded. Retry in 12s.",
  "error": "Too Many Requests",
  "statusCode": 429,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** waiting the number of seconds in the `Retry-After` response header, never more than
60, then retrying. The refused request did nothing, so retrying it is safe even for a write. To
avoid the 429 in the first place, pace bulk jobs with the `X-RateLimit-Remaining` and
`X-RateLimit-Reset` headers, which come on every response to an authenticated request. Because the budget is shared, a burst
from one integration can throttle another integration on the same team.

### 500 Internal Server Error

Something failed on the Salesfinity side.

```json theme={"system"}
{
  "message": "Internal server error",
  "error": "Internal Server Error",
  "statusCode": 500,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** retrying with exponential backoff. If it persists, email
[hello@salesfinity.co](mailto:hello@salesfinity.co) with the `request_id`, the request path and the
timestamp.

### 502 Bad Gateway and 504 Gateway Timeout

The Sequencer and enrichment endpoints pass requests on to a Salesfinity service behind the API.
**502** means that service could not be reached. **504** means the Sequencer did not answer within
55 seconds.

```json theme={"system"}
{
  "message": "Sequencer service timed out",
  "error": "Gateway Timeout",
  "statusCode": 504,
  "request_id": "0f6d3c52-9f3e-4c1a-a4b8-2b1f7e5d9c10"
}
```

**Recover by** retrying with exponential backoff. After a 504 on a write, the write may still have
finished, so confirm with a `GET` before retrying a create.

### Other statuses

During planned maintenance, `/v2/sequencer` routes answer **405** with `error`
`Method Not Allowed` and a `message` that says maintenance is in progress. Retry later.

The first time a sequence with email steps is enabled, Salesfinity sets up email sending for the
team. If that briefly fails, [Enable a sequence](/api-reference/endpoint/enable-sequencer-sequence)
answers **503** `Service Unavailable`; retry after a moment.

## Which errors are retryable

| Status | Retryable | Notes |
| - | - | - |
| 400 | No | The request is malformed; an identical retry fails identically |
| 402 | No | Requires more team enrichment credits first |
| 403 | No | Requires a valid key, or a `user_id` allowed to act |
| 404 | No | Requires a correct path or ID |
| 405 | Later | Sequencer maintenance; retry after a few minutes |
| 409 | Sometimes | Retry with the same `Idempotency-Key` if the first request is still running; otherwise fix the request or the record |
| 413 | No | Requires a smaller body |
| 422 | No | Requires fixing what `message` names |
| 429 | **Yes** | Wait for the seconds in `Retry-After`, then retry |
| 500 | **Yes** | Back off with exponential jitter |
| 502, 504 | **Yes** | Back off; confirm state before retrying a create |
| 503 | **Yes** | Retry after a moment |

## Retrying writes safely

Outside the Sequencer, `POST` endpoints are not idempotent: sending the same request twice acts
twice. Cap retries on writes, and confirm state with a `GET` before retrying a create so you do not
duplicate a record.

Every `POST /v2/sequencer/*` endpoint except the three `/preview` endpoints accepts an
`Idempotency-Key` header, which makes a retry safe. Generate a unique value, such as a UUID, for
each write, and send the same value when you retry that write. `PATCH`, `PUT` and `DELETE`
requests do not take one.

* A retry with the same key and the same request returns the original successful response without
  acting again.
* The same key with a different body or path returns **409**. The comparison covers the method,
  the path including its query string, and the JSON body.
* The same key while the first request is still running returns **409**. Retry after a moment.
* A request that fails frees its key, so the retry runs normally. That includes a 5xx: after a
  **500**, **502** or **504** on a create, the work may have finished behind the error, so confirm
  with a `GET` before retrying.
* Keys are kept for 24 hours and are scoped to your team.
* A 429 is not a failure of the write: the request was refused before the key was claimed, so the
  retry is the first attempt.

```bash theme={"system"}
curl -X POST https://client-api.salesfinity.co/v2/sequencer/tasks \
  -H 'x-api-key: YOUR_API_KEY' -H 'content-type: application/json' \
  -H 'Idempotency-Key: 5d0e2c4a-6f1b-4f7e-9a3c-8b2d1e0f7a64' \
  -d '{ "user_id": "USER_ID", "action": "call", "contact": "CONTACT_ID", "due_at": "DUE_AT" }'
```

Replace `DUE_AT` with an ISO 8601 time within the next 365 days, for example
`2026-11-02T15:00:00Z`.

## Handling errors in code

<CodeGroup>
  ```js Node.js theme={"system"}
  async function salesfinity(path, init = {}) {
    const res = await fetch(`https://client-api.salesfinity.co${path}`, {
      ...init,
      headers: { "x-api-key": process.env.SALESFINITY_API_KEY, ...init.headers },
    });

    if (res.ok) return res.json();

    // Every API error is JSON with the same envelope. A failure at the network
    // edge may not be, so fall back to the status line.
    const body = await res.json().catch(() => ({}));
    const statusCode = body.statusCode ?? res.status;
    const error = body.error ?? res.statusText;
    const request_id = body.request_id ?? res.headers.get("x-request-id");
    const detail = Array.isArray(body.message) ? body.message.join("; ") : body.message;

    const err = new Error(`Salesfinity ${statusCode} ${error}: ${detail} (request ${request_id})`);
    err.statusCode = statusCode;
    err.requestId = request_id;
    err.retryable = statusCode === 429 || statusCode >= 500;
    // On a 429, the number of seconds to wait before retrying.
    err.retryAfter = Number(res.headers.get("retry-after")) || undefined;
    throw err;
  }
  ```

  ```python Python theme={"system"}
  import os, requests

  class SalesfinityError(Exception):
      def __init__(self, res):
          try:
              body = res.json()
          except ValueError:
              # A failure at the network edge may not be JSON.
              body = {}
          self.status_code = body.get("statusCode", res.status_code)
          self.error = body.get("error", res.reason)
          self.request_id = body.get("request_id") or res.headers.get("X-Request-Id")
          message = body.get("message")
          self.detail = "; ".join(message) if isinstance(message, list) else message
          self.retryable = self.status_code == 429 or self.status_code >= 500
          # On a 429, the number of seconds to wait before retrying.
          self.retry_after = int(res.headers.get("Retry-After", 0)) or None
          super().__init__(
              f"Salesfinity {self.status_code} {self.error}: {self.detail} (request {self.request_id})"
          )


  def salesfinity(path, **kwargs):
      res = requests.get(
          f"https://client-api.salesfinity.co{path}",
          headers={"x-api-key": os.environ["SALESFINITY_API_KEY"]},
          timeout=60,
          **kwargs,
      )
      if not res.ok:
          raise SalesfinityError(res)
      return res.json()
  ```
</CodeGroup>

## Reporting a problem

Email [hello@salesfinity.co](mailto:hello@salesfinity.co) or use the
[help center](https://support.salesfinity.ai). Include:

* The `request_id` from the error body, or the `X-Request-Id` response header. It lets Salesfinity
  find the exact request.
* The request method and path, for example `GET /v1/call-log`.
* The timestamp of the request, with its timezone.
* The `error` and `statusCode` from the response body.

Never include your API key in a support message.

## Next steps

<CardGroup cols={2}>
  <Card title="Stay under the rate limits" icon="gauge" href="/api-reference/rate-limits">
    Per-team budgets, the headers that report them, and pacing.
  </Card>

  <Card title="Test safely on live data" icon="flask" href="/developers#test-safely">
    Read-only first, a test team, and writes that reach the dialer.
  </Card>
</CardGroup>


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