# Errors

Source: https://solidhooks.com/docs/errors

> The error shapes the API and ingest endpoint return, and a reference for every error code with what to do about it.

The management API and the ingest endpoint use different envelopes. Both carry a machine-readable `code`, so branch on `code`, not on the message text.

## Management API envelope

Errors from `https://api.solidhooks.com/v1` come from oRPC and carry a `code`, a `message` and, for some errors, a `data` object with typed details:

```json
{
  "code": "QUOTA_EXCEEDED",
  "status": 429,
  "message": "Monthly delivery quota exceeded (10000/10000). Resets at 2026-11-01T00:00:00.000Z. Upgrade your plan to continue.",
  "data": { "limit": 10000, "current": 10000, "resetAt": "2026-11-01T00:00:00.000Z" }
}
```

Validation failures (`BAD_REQUEST`) include field-level details in the body.

## Ingest envelope

Failures at `https://sdhk.events/<maskId>` use a flat envelope. `data` appears only when the error has typed details:

```json
{ "ok": false, "code": "UNAUTHORIZED", "message": "Signature verification failed" }
```

Successful ingest returns `{"ok":true,"eventId":"evt_..."}`, or `{"ok":true,"duplicate":true}` when an [idempotency](https://solidhooks.com/docs/idempotency) key matches an earlier event.

## Error reference

### 400 — Bad request

| Code                     | Where                                      | Meaning and what to do                                                                                                                                                                                                                                              |
| ------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BAD_REQUEST`            | Any endpoint, ingest                       | Validation failed; the body names the fields. Also returned when you replay a `duplicate` or `rejected` event, when a source ingests email and receives an HTTP webhook, and when private delivery is not configured on the deployment. Fix the request.            |
| `DESTINATION_NOT_PUBLIC` | `POST /v1/destinations/{id}/test-delivery` | The URL resolves to a private or reserved range (loopback, RFC1918, CGNAT, link-local including cloud metadata), so SolidHooks will not dial it. Use a public address, or make the destination `private` and deliver through a [connector](https://solidhooks.com/docs/private-delivery). |

### 401 — Unauthorized

| Code           | Where                                                                     | Meaning and what to do                                                                                                                                                                          |
| -------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHORIZED` | `/v1`                                                                     | The API key is missing, invalid, expired or revoked. An agent token also gets 401 here; agents use [capability execution](https://solidhooks.com/docs/agent-auth). Ask the account owner whether the key was revoked. |
| `UNAUTHORIZED` | Ingest                                                                    | Signature verification failed. The message is always `Signature verification failed`, on purpose. The real reason is stored on the event; see below.                                            |
| `UNAUTHORIZED` | `POST /v1/connectors/bootstrap`, `POST /v1/connectors/replicas/heartbeat` | Unknown or revoked enrolment key or heartbeat token. The message is deliberately generic.                                                                                                       |

### 403 — Forbidden

| Code                               | Where                                    | Meaning and what to do                                                                                                                                                                                                            |
| ---------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FORBIDDEN`                        | `/v1`, capability calls                  | No organisation or project could be resolved. For an agent, this is what you get before calling `account.select_project`.                                                                                                         |
| `INSUFFICIENT_API_KEY_PERMISSIONS` | `/v1`                                    | The key's own permissions do not cover this action: the resource was not granted `read & write`, or not granted at all. Retrying with the same key will not help. Ask whoever holds the key to mint one scoped for this resource. |
| `CONNECTOR_LIMIT_REACHED`          | `POST /v1/connectors`                    | The plan's connector limit is used up. `data: { limit, current }`. Upgrade or remove an unused connector.                                                                                                                         |
| `REPLICA_LIMIT_REACHED`            | `POST /v1/connectors/bootstrap`          | The plan's replica limit for this connector is used up. `data: { limit, current }`. Upgrade or remove a dead replica.                                                                                                             |
| `CONNECTOR_REPLICA_REVOKED`        | `POST /v1/connectors/replicas/heartbeat` | The token matches a revoked replica. `data` carries a `reason`: `connector_deleted`, `replica_revoked` or `identity_absent`. The stock agent stops and does not re-enrol.                                                         |
| `NEGOTIATED_PLAN_OVERRIDE`         | Checkout, via `/auth/*`                  | The organisation is on a negotiated plan, so new self-serve checkout is blocked. Contact support.                                                                                                                                 |

### 404 — Not found

| Code        | Where        | Meaning and what to do                                                                                                                                                                   |
| ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NOT_FOUND` | Any resource | The resource does not exist, or belongs to another organisation or project. A foreign `X-Project-Id` also returns this. On a project-bound key, check which project the key is bound to. |

### 409 — Conflict

| Code                      | Where                                                                                     | Meaning and what to do                                                                                                                                                                                                                 |
| ------------------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CONFLICT`                | `POST /v1/deliveries/{id}/retry`                                                          | The delivery is `throttled`: it is queued behind a rate limit and will send on its own. Do not retry it.                                                                                                                               |
| `CONNECTOR_PROVISIONING`  | `DELETE /v1/connectors/{id}`                                                              | The connector is still being provisioned (`provisionedAt` is `null`). Retry in a moment.                                                                                                                                               |
| `ROTATION_IN_USE`         | `POST /v1/sources/{id}/cancel-rotation`                                                   | The new secret has already verified requests, so reverting would reject traffic the producer is signing correctly. `data: { count, lastSeenAt }`. Re-send with `force: true` only if you are sure.                                     |
| `ROTATION_OVERLAP_ACTIVE` | `POST /v1/sources/{id}/rotate-secret`, `POST /v1/destinations/{id}/rotate-signing-secret` | The previous secret is still in its overlap window. `data: { expiresAt }`. Wait for it to close, or cancel the source rotation or end the destination overlap first. A destination rotation also accepts `force: true` to displace it. |

### 415 and 413 — Body problems at ingest

These two come from the ingest code rather than the agent guide.

| Code                | Where  | Meaning and what to do                                                                    |
| ------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `UNSUPPORTED_BODY`  | Ingest | The body contains NUL bytes. Binary payloads are not supported; send a text-encoded body. |
| `PAYLOAD_TOO_LARGE` | Ingest | The body exceeds the source's `maxPayloadSizeKb`.                                         |

### 429 — Limits

| Code             | Where                                          | Meaning and what to do                                                                                                                                                                                                                            |
| ---------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QUOTA_EXCEEDED` | Ingest, `POST /v1/events/{id}/replay`          | The organisation used its delivery quota for the current window. Only hard-capped plans return this; see [Plans and usage](https://solidhooks.com/docs/plans). `data: { limit, current, resetAt }`. Wait for `resetAt` or upgrade.                                      |
| `RATE_LIMITED`   | Ingest, `POST /v1/sources/{id}/test-signature` | Throughput limit hit. `data: { limit, remaining, retryAfterSeconds }`. On the direct ingest route, `Retry-After`, `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers are also set. Back off and retry. See [Rate limits](https://solidhooks.com/docs/rate-limits). |
| (no code)        | `/v1`                                          | API keys are limited to 600 requests per minute per key.                                                                                                                                                                                          |

The public connector endpoints are limited to 5 requests per second per client IP with a burst of 20, applied before the token is checked.

### 503 — Unavailable

| Code                         | Where                                   | Meaning and what to do                                                                                                                                                       |
| ---------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ROTATION_USAGE_UNAVAILABLE` | `POST /v1/sources/{id}/cancel-rotation` | The per-secret counters could not be read, so SolidHooks cannot tell whether the producer has switched. The revert is refused. Retry shortly, or re-send with `force: true`. |

> **Seat limits**
>
> The agent guide lists `SEAT_LIMIT_REACHED` for inviting a member past the plan's seat cap. That invitation runs through the dashboard's `/auth/*` endpoints, not `/v1`, and the code in the repository is `INVITATION_LIMIT_REACHED`, returned as a 403.

## Why ingest 401s are opaque

An unauthenticated caller gets the same `Signature verification failed` message whichever check failed. Telling them why would turn the 401 into a signature oracle. The request is instead stored as an event with `status: "rejected"` and a `rejectionReason`. Read it with `GET /v1/events?status=rejected`.

| `rejectionReason`            | Meaning                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| `signature_mismatch`         | The signature did not match the secret.                                            |
| `missing_signature_header`   | The signature header was absent.                                                   |
| `missing_id_header`          | Standard Webhooks only: the `webhook-id` header, which the MAC covers, was absent. |
| `missing_timestamp_header`   | The timestamp header was absent.                                                   |
| `malformed_signature_header` | The signature header could not be parsed.                                          |
| `malformed_timestamp`        | The timestamp could not be parsed.                                                 |
| `timestamp_out_of_tolerance` | The timestamp fell outside the replay window.                                      |
| `verifier_misconfigured`     | The source's verifier config is unusable.                                          |
| `unsupported_verifier`       | The source uses a verifier type that is not supported.                             |

Rejected events are never deduplicated and never delivered. They are rate-limited per source, so under a flood the log is a representative sample, not a full ledger. To find out why a specific request fails without waiting for traffic, call `POST /v1/sources/{id}/test-signature`, which does return the reason. See [Verification](https://solidhooks.com/docs/verification).

## Next steps

* [Verification](https://solidhooks.com/docs/verification) — signature schemes and rejection diagnosis
* [Rate limits](https://solidhooks.com/docs/rate-limits) — ingest and delivery pacing
* [Plans and usage](https://solidhooks.com/docs/plans) — quotas and entitlements