SolidHooksSolidHooks/ 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:

{
  "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:

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

Successful ingest returns {"ok":true,"eventId":"evt_..."}, or {"ok":true,"duplicate":true} when an idempotency key matches an earlier event.

Error reference

400 — Bad request

CodeWhereMeaning and what to do
BAD_REQUESTAny endpoint, ingestValidation 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_PUBLICPOST /v1/destinations/{id}/test-deliveryThe 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.

401 — Unauthorized

CodeWhereMeaning and what to do
UNAUTHORIZED/v1The API key is missing, invalid, expired or revoked. An agent token also gets 401 here; agents use capability execution. Ask the account owner whether the key was revoked.
UNAUTHORIZEDIngestSignature verification failed. The message is always Signature verification failed, on purpose. The real reason is stored on the event; see below.
UNAUTHORIZEDPOST /v1/connectors/bootstrap, POST /v1/connectors/replicas/heartbeatUnknown or revoked enrolment key or heartbeat token. The message is deliberately generic.

403 — Forbidden

CodeWhereMeaning and what to do
FORBIDDEN/v1, capability callsNo organisation or project could be resolved. For an agent, this is what you get before calling account.select_project.
INSUFFICIENT_API_KEY_PERMISSIONS/v1The 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_REACHEDPOST /v1/connectorsThe plan's connector limit is used up. data: { limit, current }. Upgrade or remove an unused connector.
REPLICA_LIMIT_REACHEDPOST /v1/connectors/bootstrapThe plan's replica limit for this connector is used up. data: { limit, current }. Upgrade or remove a dead replica.
CONNECTOR_REPLICA_REVOKEDPOST /v1/connectors/replicas/heartbeatThe 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_OVERRIDECheckout, via /auth/*The organisation is on a negotiated plan, so new self-serve checkout is blocked. Contact support.

404 — Not found

CodeWhereMeaning and what to do
NOT_FOUNDAny resourceThe 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

CodeWhereMeaning and what to do
CONFLICTPOST /v1/deliveries/{id}/retryThe delivery is throttled: it is queued behind a rate limit and will send on its own. Do not retry it.
CONNECTOR_PROVISIONINGDELETE /v1/connectors/{id}The connector is still being provisioned (provisionedAt is null). Retry in a moment.
ROTATION_IN_USEPOST /v1/sources/{id}/cancel-rotationThe 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_ACTIVEPOST /v1/sources/{id}/rotate-secret, POST /v1/destinations/{id}/rotate-signing-secretThe 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.

CodeWhereMeaning and what to do
UNSUPPORTED_BODYIngestThe body contains NUL bytes. Binary payloads are not supported; send a text-encoded body.
PAYLOAD_TOO_LARGEIngestThe body exceeds the source's maxPayloadSizeKb.

429 — Limits

CodeWhereMeaning and what to do
QUOTA_EXCEEDEDIngest, POST /v1/events/{id}/replayThe organisation used its delivery quota for the current window. Only hard-capped plans return this; see Plans and usage. data: { limit, current, resetAt }. Wait for resetAt or upgrade.
RATE_LIMITEDIngest, POST /v1/sources/{id}/test-signatureThroughput 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.
(no code)/v1API 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

CodeWhereMeaning and what to do
ROTATION_USAGE_UNAVAILABLEPOST /v1/sources/{id}/cancel-rotationThe 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.

rejectionReasonMeaning
signature_mismatchThe signature did not match the secret.
missing_signature_headerThe signature header was absent.
missing_id_headerStandard Webhooks only: the webhook-id header, which the MAC covers, was absent.
missing_timestamp_headerThe timestamp header was absent.
malformed_signature_headerThe signature header could not be parsed.
malformed_timestampThe timestamp could not be parsed.
timestamp_out_of_toleranceThe timestamp fell outside the replay window.
verifier_misconfiguredThe source's verifier config is unusable.
unsupported_verifierThe 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.

Next steps