API conventions
What holds everywhere, rather than repeated on every route.
The error envelope
Every error has the same shape.
{
"status": "error",
"message": "CREDITS_EXHAUSTED",
"messageDetail": "Not enough credits."
}message carries an uppercase machine code, to read in your code. messageDetail carries a readable sentence, to show or to log. Never branch your logic on messageDetail: it is prose, it can change.
The full list is on the Error codes page.
The request_id
Every response carries an x-request-id header. It is the only thing worth quoting if you open a ticket: it locates the exact call in the logs.
The libraries put it on every exception and every result.
except SpavikError as exc:
logger.error("call refused", code=exc.code, request_id=exc.request_id)Idempotency
The five operations that change something accept an Idempotency-Key header: POST /v1/models, /predict, /outcomes, /refresh and POST /v1/forecast.
Replaying the same call with the same key returns the already computed response, with no second charge. That is what makes retrying after a network cut safe: without it, you would choose between paying twice and losing a result you already paid for.
curl -X POST "$SPAVIK_BASE_URL/v1/models/{id}/predict" \
-H "X-API-Key: $SPAVIK_API_KEY" \
-H 'Idempotency-Key: 6f1c8b90-...' \
-H 'Content-Type: application/json' -d '{"rows":[...]}'Draw one key per logical call, not per attempt: replaying it unchanged is what protects you. The libraries handle this.
Two codes relate to it: IDEMPOTENCY_KEY_REUSED if you reuse a key for different content, REQUEST_IN_PROGRESS if the first call has not answered yet.
Pagination
Lists are walked with limit and offset, and return a pagination object.
{
"data": [ ... ],
"pagination": { "total": 128, "limit": 50, "offset": 0, "has_more": true }
}| Route | Default limit | Maximum |
|---|---|---|
GET /v1/models, /v1/api-keys | 50 | 200 |
GET /v1/requests | 100 | 500 |
The libraries hide all of this: models() is an iterator that fetches pages as it goes.
for model in spavik.models():
print(model.id)Time windows
Routes that read history accept from and to as ISO 8601 dates, or days for a rolling window.
| Parameter | Default | Maximum |
|---|---|---|
days | 30 | 365 |
GET /v1/requests additionally accepts status=error, which keeps only failures: enough to track down a rejection that happened in production without reproducing it.
Retries
Three families deserve a retry, and only three: 429, the 5xx, and network failures. A 4xx will not change its mind; replaying it only wastes time.
The API exposes no Retry-After header today. The libraries therefore back off exponentially with jitter, three attempts at most, replaying the same idempotency key.
Identifiers
They are opaque and prefixed. Do not parse them, do not rebuild them.
| Prefix | Object |
|---|---|
mdl_ | model |
ver_ | model version |
whk_ | webhook |
sk-spv-api- | API key |
sk-spv-admin- | admin key |
Dates
All dates are ISO 8601, in UTC, with the Z suffix.