Errors and debugging
Every error code, what it actually means, and what to send us when you are stuck.
The envelope
Every failure has the same shape:
{
"error": {
"code": "insufficient_scope",
"message": "This API key lacks the datasets:write scope.",
"details": [],
"request_id": "31592257-1558-48ed-8328-a5086ccfa4ed"
}
}Branch on code. It is stable for a given condition. message is prose for a
human reading logs and may be reworded.
Codes
| Status | code | Means | First thing to check |
|---|---|---|---|
| 400 | invalid_request | Malformed body or parameters | details — it names the field |
| 401 | unauthorized | Missing, malformed, expired or revoked key | Is x-api-key set? Is the key live? |
| 403 | insufficient_scope | Authenticated, but the key lacks the scope | The message names the scope |
| 403 | forbidden | Scoped, but still not allowed | Rare. Usually an ownership rule |
| 404 | not_found | No such resource — or it is not yours | The id, and the key's workspace |
| 409 | conflict | A state conflict | The message says which state |
| 413 | payload_too_large | Body exceeded the limit | Batch size |
| 429 | rate_limited | Too many attempts | Retry-After |
| 500 | internal_error | Our fault | Send us the request_id |
The three that confuse people
Validation details
On a 400, details carries per-field messages:
"details": [
{ "field": "channels.0.name", "message": "String must contain at least 1 character(s)" },
{ "field": "rows.12.values", "message": "Expected 3 values, received 2" }
]The path is into the body you sent, so rows.12.values is the thirteenth row.
Request ids
Send X-Request-Id and it is echoed back and used in our logs. Send nothing and
one is generated. Either way it appears on every response — success as well as
failure — and inside every error body.
curl -si https://app.xpectraflow.com/api/experiments/list \
-H "x-api-key: $XPECTRA_API_KEY" \
-H "X-Request-Id: nightly-sync-42"Using your own ids — a job name, a run number — makes your logs and ours line up without correlating timestamps.
Debugging a webhook that never arrives
Covered step by step on
Lifecycle and retries.
The short version: check the command's status first, then
GET /api/webhooks/deliveries?commandId=…, and remember that a 4xx from your
receiver is final.
Debugging a signature that will not verify
In order of likelihood:
You are signing a re-serialised body. The signature covers the raw request bytes. If your framework parsed the JSON and you re-encoded it, key order and number formatting differ. Use the raw body.
You stripped whsec_ before keying. The key is the whole secret string,
prefix included.
You are only checking the first v1= value. During a
rotation two are sent, and either may be the
one you know.
Your clock is off. The timestamp is inside the signed string and must be within ±300 seconds.
What to send us
- The
request_id - The
codeand the full error body - The endpoint and roughly when
- The key's
keyPrefix— never the key itself
With a request_id we can find the exact request. Without one, we are
correlating timestamps.