Errors and retries

What each status code means, and what a client does with it.

The error shape

An error is JSON with a short code, one plain sentence, and the field it is about when it is about one:

json
{
  "code": "permission_denied",
  "message": "this connection does not hold that permission on this company"
}

Branch on the status and on code. Log message or show it to a person, and do not parse it.

Status codes

Every status the contract defines, read from the contract when these pages were built:

StatusReturned byWhat it means
200GET /v1/samplesThe samples, and the window they were read over.
202POST /v1/ingestStored. The receipt names the artifact and the version it will be parsed under.
POST /v1/webhooks/{source_id}Stored. The receipt names the artifact. A redelivery with the same delivery id answers with the first receipt and stores nothing.
400POST /v1/ingestThe body is not well formed, a field is missing, or the credential was put in the URL.
GET /v1/samplesfrom or to is not a date, or from is not before to.
POST /v1/webhooks/{source_id}The signature header or the delivery id is not well formed, or the signature's time is outside the window. Do not retry as is.
401POST /v1/ingestThe token or the static credential is missing, unknown, revoked or expired. With client credentials, ask for a new token once and send again. A second 401 is final.
GET /v1/samplesThe token or the static credential is missing, unknown, revoked or expired. With client credentials, ask for a new token once and send again. A second 401 is final.
POST /v1/webhooks/{source_id}There is no Norra-Signature header. Do not retry.
403POST /v1/ingestThe connection works and does not hold sources.ingest (`permission_denied`, with `WWW-Authenticate: Bearer error="insufficient_scope", scope="sources.ingest"`), or a production connection was sent from the documentation site (`docs_origin_needs_sandbox`), or the source takes signed webhooks only (`webhook_only`).
GET /v1/samplesThe connection works and does not hold metrics.read (`permission_denied`, with `WWW-Authenticate: Bearer error="insufficient_scope", scope="metrics.read"`), or a production connection was sent from the documentation site (`docs_origin_needs_sandbox`).
404POST /v1/ingestThe source is not the company's, or the connection has nothing on the company.
GET /v1/samplesThe source is not the company's, or the connection has nothing on the company.
POST /v1/webhooks/{source_id}Nothing at this address accepts that signature. Do not retry.
409POST /v1/ingestThe company has paused this source (`source_paused`). Nothing in the request is kept, and Norra does not fetch it later. Do not retry on a schedule. Send it again once the source is resumed. Or the idempotency key already stored a different body (`key_reused`). Nothing in the request is kept, and the first delivery stands. Do not retry.
POST /v1/webhooks/{source_id}The company has paused this source (`source_paused`). Nothing in the request is kept, and Norra does not fetch it later. Do not retry on a schedule. Send it again once the source is resumed. Or the idempotency key already stored a different body (`key_reused`). Nothing in the request is kept, and the first delivery stands. Do not retry.
413POST /v1/ingestThe delivery is larger than this endpoint accepts.
POST /v1/webhooks/{source_id}The body is larger than this endpoint accepts. Do not retry.
415POST /v1/webhooks/{source_id}The body is not JSON or CSV. Do not retry.
429POST /v1/ingestToo many deliveries on this connection. Wait the number of seconds in Retry-After.
GET /v1/samplesToo many requests on this connection. Wait the number of seconds in Retry-After.
POST /v1/webhooks/{source_id}Too many deliveries on this source. Retry after the number of seconds in Retry-After.
5XXPOST /v1/webhooks/{source_id}Norra could not take the delivery. Retry with backoff.

What to do with each

  • 200: the read worked. If truncated is true, ask again for a narrower window. If window.narrowed is true, the window was moved forward to the limit.
  • 202: the delivery is stored. Keep the artifact_id.
  • 400: the request is wrong, and sending it again gets the same answer. field names what to fix. The code credential_in_url means the credential was put in the URL: move it to the Authorization header, and rotate it, because the URL may already be in a log.
  • 401: the token or static key is missing, unknown, revoked or expired. With client credentials, ask for a new token once and send again. Otherwise, and after a second 401, do not retry. Check the header, then the connection's state on the Developer page.
  • 401 from the token endpoint with invalid_client: the client id or secret is wrong, or the connection is revoked or expired. Do not retry.
  • 403 with the code permission_denied: the connection does not hold the permission the call needs. The WWW-Authenticate header names it. Do not retry. Make a connection that may do what the operation needs.
  • 403 with the code docs_origin_needs_sandbox: the request came from the documentation site with a production connection. Use a sandbox connection or the demo connection there.
  • 403 with the code webhook_only: the source takes signed webhooks only, so nothing can push to it.
  • 404: the source is not the company's, or the connection has nothing on the company.
  • 409 with the code source_paused: the company has paused the source. Nothing in the request is kept, and Norra will not fetch it later. Stop sending. Once the source is resumed, send what was refused again, with the same idempotency keys. A webhook sender gets the same answer.
  • 409 with the code key_reused: the idempotency key already stored a delivery with a different body. Nothing in this request is kept, and the first delivery stands. Do not retry it: a retry sends the same body under the same key, and a key names one body.
  • 413: the delivery is over 8388608 bytes (8 MiB). Split it into smaller deliveries, each with its own idempotency key.
  • 429: too many requests on this connection. Wait the number of seconds in Retry-After, then send the same request again.
  • A 5xx, a timeout or a dropped connection: send the same request again, with the same idempotency key.

Retrying safely

A push carries an idempotency key, so sending it again stores nothing twice and returns the same receipt. Every push is safe to retry as long as the retry reuses the key and sends the same body. The same key with a different body is refused with 409 and key_reused.

  • Retry a 429, a 5xx and a timeout. Do not retry another 4xx unchanged.
  • Wait longer before each attempt, for example 1, 2, 4 and 8 seconds with some randomness added, up to a limit you choose.
  • After a 429, wait at least Retry-After seconds.

Rate limits

A connection may make 60 requests a minute, reads and pushes together, bursting to 20. The 200, 202 and 429 answers carry three headers:

  • RateLimit-Limit: requests allowed in the window.
  • RateLimit-Remaining: requests left in the window.
  • RateLimit-Reset: seconds until the window refills.

A client that reads RateLimit-Remaining can slow down before it gets a 429.