HTTP status codes and the error body the API returns
| Code | Name | Description | How to Fix |
|---|---|---|---|
| 400 | Bad Request | The request is malformed, or ingest is missing its Idempotency-Key header. | Send an Idempotency-Key (255 characters at most) on every ingest POST and check the request against the API reference. |
| 401 | Unauthorized | The API key is missing or not recognized, or an ingest request has a missing or wrong X-Webhook-Signature. | Send the key in X-RegEngine-API-Key (an Authorization header is not accepted). Sign the exact body bytes you send. |
| 402 | Payment Required | The workspace has no active subscription. Ingest and the FDA endpoints check this; record reads do not. | Contact RegEngine. A retry will not help until the workspace subscription is active. |
| 403 | Forbidden | The key lacks the scope this endpoint needs, or an X-Tenant-ID or tenant_id value names a different workspace than the key. | Check the key scopes on the Authentication page and stop sending X-Tenant-ID. |
| 404 | Not Found | The resource does not exist for your workspace, or the path is not part of the public API. | Check the path against the API reference and the resource ID. |
| 405 | Method Not Allowed | The path exists but not for this HTTP method, for example GET on /webhooks/ingest. | Use the method shown in the API reference. |
| 409 | Conflict | An Idempotency-Key was reused with different request bytes, or its first request is still in progress. | Use a new Idempotency-Key for a changed payload. For an in-progress key, see Retry Strategy. |
| 422 | Unprocessable Entity | The body is well-formed JSON but failed validation, for example an event timestamp more than 24 hours ahead. The whole request is refused and nothing is stored. The details array lists each failing field. | Fix the fields named in error.details and send the request again. |
| 429 | Too Many Requests | A rate limit was exceeded. | Stop sending for 60 seconds, then retry. See Rate Limits. |
| Code | Name | Description | How to Fix |
|---|---|---|---|
| 500 | Internal Server Error | Unexpected server error. The message is deliberately generic. | Retry with exponential backoff. If it persists, contact support with the time of the request. |
| 502 | Bad Gateway | The gateway could not reach the API. | Retry with exponential backoff. |
| 503 | Service Unavailable | A dependency the request needs, such as the database or the subscription check, is unavailable. | Retry with exponential backoff. |
| 504 | Gateway Timeout | The request took too long to process. | Send fewer events per request and retry. |
Errors return a JSON body with an error object that has a type and a message. There is no separate machine-readable code list: branch on the HTTP status.
{
"error": {
"type": "http_402",
"message": "No active subscription found for this tenant. If you just signed up, wait a few seconds and retry. Otherwise visit https://regengine.co/billing."
}
}| When | What the body looks like |
|---|---|
| Most errors (400, 401, 402, 403, 404, 405, 429, 500, 503) | error.type is "http_" plus the status, for example http_402. error.message says what happened. |
| 400 from a bad value | error.type is bad_request and error.message is "Invalid request". |
| 422 validation failure | error.type is validation_error. error.details lists each failing field. |
| 401 on ingest signature | error.type is http_401 and an extra error.error field is missing_webhook_signature or invalid_webhook_signature. The invalid case has a generic message on purpose. |
| Unhandled 500 | error.type is internal_error. error.correlation_id and error.request_id are included; quote them to support. |
| Idempotency responses (409, 413, 503, 504) | Not an object: {"error": "idempotency_key_conflict"}. The other values are idempotency_request_in_progress, payload_too_large, idempotency_backend_unavailable and request_timeout. |
| Some 429 responses | Not an object: {"error": "rate_limit_exceeded", "detail": "..."}. Other 429s use the usual error object. |
Best Practice
For 5xx errors, use exponential backoff with jitter: start at 1 second, double each retry, cap at 32 seconds. For a 429, stop for 60 seconds. Retrying an ingest request is safe when you repeat the same Idempotency-Key with the same bytes, because only successful responses are stored and a stored one is replayed. If a retry returns 409 idempotency_request_in_progress, the first attempt may still be running or its outcome is unknown. Check GET /api/v1/records?tlc=<lot code> before you send the batch again under a new key.
import random
import time
import requests
def post_with_retry(url, headers, body, max_retries=5):
"""Retry one ingest request. headers carries a single Idempotency-Key and body is the
exact signed bytes, so every retry repeats the same request."""
for attempt in range(max_retries):
response = requests.post(url, headers=headers, data=body)
if response.status_code == 429:
time.sleep(60) # stop for 60 seconds; do not rely on rate-limit headers
continue
if response.status_code in (500, 502, 503, 504):
time.sleep(min(32, 2 ** attempt) + random.uniform(0, 1))
continue
return response
raise RuntimeError("Max retries exceeded")