Errors
The error envelope, every error code with its HTTP status and fix, the details shapes, Retry-After, and the order of the checks.
The envelope#
Every refusal is JSON:
json
{ "error": { "code": "connection_not_active", "message": "The connection is paused.", "details": { "status": "paused" } }, "request_id": "…" }- Branch on
code, never onmessage: messages are English and may change. request_idequals theNK-Request-Idresponse header, which every response carries. Quote it when you write to partners@novikvadrat.rs.- Responses are
application/json; charset=utf-8withCache-Control: no-store.
Every code#
| Code | HTTP | You see it when | Fix |
|---|---|---|---|
ingest_disabled |
503 | Novi Kvadrat switched ingestion off | retry later with backoff |
body_too_large |
413 | the body is over 4 MiB (checked first) | split the batch |
signature_missing |
401 | a signing header is absent or malformed | send all five headers |
key_invalid |
401 | unknown or revoked key | use an active key, or register a new one |
key_expired |
401 | the key is past its 365 days | register a new key |
app_suspended |
403 | staff suspended the app | write to partners@novikvadrat.rs |
account_blocked |
403 | staff suspended or archived your organization | write to partners@novikvadrat.rs |
ip_allowlist_empty |
403 | a live app without addresses | add addresses to the allowlist |
ip_not_allowed |
403 | the caller is not listed, or its address can't be determined | edit the allowlist |
digest_mismatch |
400 | the body changed after hashing | hash the exact bytes you send |
timestamp_out_of_window |
401 | your clock is off by more than 300 s | sync the clock (NTP) |
signature_invalid |
401 | the base string or the key does not match | compare with the test vector |
rate_limited |
429 | over the app's per-minute budget (replays count too) | wait Retry-After |
nonce_reused |
401 | a nonce sent twice within 15 minutes | a fresh random nonce per request |
connection_not_found |
404 | unknown id, or another app's connection | check the id and the key's environment |
connection_not_active |
409 | details.status is pending, paused or revoked |
wait for the agency, or stop pushing |
connection_item_limit |
429 | over the connection's hourly item budget | wait Retry-After, spread pushes |
connect_code_invalid |
404 | an unknown, expired, used or other app's code | ask for a new code |
agency_not_eligible |
422 | details.reason is register_number_missing or account_blocked |
the agency fixes its profile |
validation_failed |
422 | the payload is invalid (details array) |
fix the fields at the pointers |
body_invalid |
400 | the body is not valid JSON | send valid JSON (valid JSON of the wrong shape, such as [], is 422 validation_failed) |
duplicate_external_id |
422 | one external_id twice in a batch |
send each listing once |
batch_too_large |
422 | more than max_batch_items, more than 50 to validate, more than 5,000 reconcile ids |
split |
idempotency_in_progress |
409 | the same key while the first request runs (details.batch_id) |
poll the batch |
idempotency_key_reused |
422 | the same key with a different body, within 7 days | a new key per distinct body |
mass_removal_requires_confirmation |
409 | a reconcile would remove too much | check, then confirm |
listing_not_found |
404 | no staged listing with that external_id on this connection |
— |
batch_not_found |
404 | unknown batch id, or another app's batch | — |
place_not_found |
404 | unknown place id | — |
internal_error |
500 | something failed on our side | retry with backoff; quote NK-Request-Id if it persists |
You will not normally see connection_exists (409): it is what an agency's own portal answers when that agency is already connected to your app.
Details#
details is present only for some codes:
| Code | details |
|---|---|
validation_failed, duplicate_external_id |
an array of { pointer, code, message }; pointer is a JSON Pointer into your request body (in a batch, rooted at /listings/{i}) and code a detail code |
connection_not_active |
{ status } |
agency_not_eligible |
{ reason } — register_number_missing or account_blocked |
mass_removal_requires_confirmation |
{ live, missing, threshold_ratio, threshold_min } |
idempotency_in_progress |
{ batch_id } |
Retry-After#
429 rate_limitedcarriesRetry-After: <seconds to the next minute>.429 connection_item_limitcarriesRetry-After: <seconds to the next hour>.
The order of the checks#
Every request runs these checks in order and stops at the first refusal:
| # | Check | Refusal |
|---|---|---|
| 0 | ingestion is on | 503 ingest_disabled |
| 1 | body ≤ 4 MiB | 413 body_too_large |
| 2 | the five headers present and well-formed | 401 signature_missing |
| 3 | the key exists, is active and not expired | 401 key_invalid / key_expired |
| 4 | the app is active, your organization is not blocked, a live app has an allowlist | 403 app_suspended / account_blocked / ip_allowlist_empty |
| 5 | the client address is allowed | 403 ip_not_allowed |
| 6 | digest, timestamp, signature | 400 digest_mismatch / 401 timestamp_out_of_window / 401 signature_invalid |
| 7 | the app's per-minute budget | 429 rate_limited |
| 8 | the nonce is new | 401 nonce_reused |
| 9 | routes that name a connection: it belongs to your app and its state allows the call | 404 connection_not_found / 409 connection_not_active |
| 10 | listing writes: the connection's hourly item budget | 429 connection_item_limit |
The rate counter runs before the nonce check, so a replayed request still counts against your budget. Writes need the connection active or certifying; reads (the connection itself and the reconcile view) also admit paused.
Last updated