Responses and statuses
In short
Section titled “In short”There are two levels of answer:
- The HTTP status tells you whether SpeculaPro could process the request at all.
- When it could (
200 OK), the body gives a status for every record in the batch.
A 200 does not mean every record was accepted. Always read the per-record results.
The response body
Section titled “The response body”{ "accepted": 497, "duplicate": 1, "stale": 0, "rejected": 2, "retry": 0, "results": [ {"index": 0, "external_id": "MOMO-2026-10-09-000184223", "status": "accepted", "id": "0199c8f2-41a7-7d02-b3e5-1f6a9c0e2b47"}, {"index": 1, "external_id": "MOMO-2026-10-09-000184224", "status": "duplicate", "id": "0199c8e9-1a02-7f3b-8c21-77d0e4a9b5c6"}, {"index": 2, "external_id": "MOMO-2026-10-09-000184225", "status": "rejected", "errors": ["no FX rate for USD→GHS on or before 2026-10-09; load rates via /api/v1/ingest/fx-rates"]} ]}| Field | Meaning |
|---|---|
accepted, duplicate, stale, rejected, retry |
How many records ended in each status. They add up to the number you sent |
results |
One entry per record, in the same order as you sent them |
results[].index |
Position of the record in your items array, starting at 0 |
results[].external_id |
Your reference (for FX rates, QUOTE@DATE) |
results[].status |
See below |
results[].id |
SpeculaPro’s identifier, when the record was stored |
results[].errors |
Why the record was rejected or must be retried |
The five statuses
Section titled “The five statuses”| Status | What it means | What your sender should do |
|---|---|---|
accepted |
Stored. For transactions: safely on SpeculaPro’s durable queue | Mark it sent. Nothing more |
duplicate |
SpeculaPro already has exactly this transaction | Mark it sent. This is normal after a retry |
stale |
A customer or account update older than (or the same as) what SpeculaPro already holds | Mark it sent. If you didn’t expect it, check your version values |
rejected |
The record has a problem SpeculaPro can’t accept. Sending it again unchanged will fail again | Log it with its errors, alert whoever owns the data, fix the source, then resend |
retry |
SpeculaPro couldn’t take it right now (a temporary fault) | Send the same record again, unchanged, after a short wait |
When the whole request fails
Section titled “When the whole request fails”These responses apply to the whole request and come as Problem Details JSON with a detail message. After a 4xx, nothing in the request was stored.
| HTTP status | Message (detail) |
Cause | What to do |
|---|---|---|---|
401 |
api key required, invalid api key, unsupported authorization scheme |
Missing, wrong, expired or revoked key | Check the key. See Getting an API key |
403 |
api key lacks scope ingest:write |
The key can’t send data | Use a key with ingest:write |
403 |
code: workspace_suspended |
On the hosted service: SpeculaPro has suspended the workspace. The key is valid | Keep what you were sending and contact SpeculaPro; retrying or rotating the key changes nothing. See A suspended workspace |
404 |
The ingest role isn’t running on this server |
Ask your SpeculaPro operator | |
413 |
at most N items per request |
Only on installations where the operator has lowered the batch limit (SP_INGEST_MAX_BATCH) below 500, and you sent more than that |
Split the batch to at most N records |
413 |
request body too large |
The body is over the size limit (8 MiB by default) | Split the batch |
422 |
validation failed |
At least one record breaks the format rules (see below), or the batch has more than 500 records | Fix and resend the whole batch, split into batches of at most 500 |
503 |
ingest temporarily unavailable; retry the batch |
The queue couldn’t take any transaction | Wait and resend the whole batch unchanged |
500 |
An unexpected server fault | Wait and resend the whole batch unchanged |
After a 503 or 500, resending the batch unchanged is always safe, because records are identified by external_id: anything that did get stored comes back as duplicate (transactions) or stale (customers and accounts with the same version).
422: one bad record fails the whole batch
Section titled “422: one bad record fails the whole batch”Before looking at individual records, SpeculaPro checks the whole request against the published format: required fields present, values from the allowed lists, correctly shaped dates and codes, lengths within limits. If any record fails these checks, the whole batch is refused and nothing in it is processed.
The response lists every problem with its location:
{ "title": "Unprocessable Entity", "status": 422, "detail": "validation failed", "errors": [ { "message": "expected value to be one of \"in, out, internal\"", "location": "body.items[37].direction", "value": "inbound" } ]}body.items[37] is the 38th record you sent (counting from 0). The exact wording of message can vary.
Problems that are about the data rather than the format (a future date, a missing FX rate, an invalid phone number) are reported per record, with status rejected, and the rest of the batch goes through.
Batch limits
Section titled “Batch limits”| Limit | Default | Set by the operator with |
|---|---|---|
| Records per request | 500 | SP_INGEST_MAX_BATCH, which can only lower it. 500 is a hard ceiling: more than 500 records always fails with 422; more than a lowered limit fails with 413 |
| Request body size | 8 MiB | SP_INGEST_MAX_BODY_BYTES |
Batches of a few hundred records give the best throughput. Sending records one at a time works, but costs a round trip per record.