Errors#
Both APIs answer a failed request with an HTTP error status and a JSON body whose top-level error object uses the same field names: code, message, details[] and request_id. That is where the resemblance ends. They are two error models, not one:
- the codes are separate lists — a code means what its own API says it means;
- the Partner API's bulk endpoints report rejected rows inside a
200; the Checkout API has no batch endpoints; - both replay retries — a repeated
Idempotency-Keywith the same body gets the stored response back, withIdempotent-Replayed: true.
Branch on code, never on message, and log request_id on every failure: it is what Povito support needs to trace a call on either API.
The envelope, side by side#
Partner API#
{
"error": {
"code": "validation_error",
"message": "items cannot exceed 50 rows per request.",
"details": [{ "field": "items", "issue": "too_many" }],
"request_id": "req_01J..."
}
}request_idis on every error;details[]is included only when there is a field to point at.- If you send an
x-request-idheader, its value is used as therequest_id. - Every code and status: Partner API limits and errors.
Checkout API#
{
"error": {
"code": "amount_mismatch",
"message": "Line items net to 44000 but amount is 45000.",
"details": [{ "field": "line_items", "issue": "sum_differs" }],
"request_id": "req_5c1b0a4e-2f6d-4a0b-9d3e-7b8c1f2a3d4e"
}
}request_idis on every error and echoed in thex-request-idresponse header; send your ownx-request-id(8–64 characters ofA-Za-z0-9_-) to have it used instead.details[]is always present — empty when there is nothing to point at — and each entry names afield(dotted path,(body)for the whole body) and anissue.- Every code is enumerated in the contract's
ErrorEnvelopeschema and explained in Checkout errors, idempotency, limits.
Per-row results: read results[]#
This is the Partner API's one behaviour with no Checkout counterpart. POST /products/bulk and PATCH /inventory/bulk handle each row on its own and answer 200 even when some rows are rejected; a rejected row carries its own error:
{
"results": [
{ "external_sku": "ERP-1042", "status": "accepted", "product_id": "prod_01J..." },
{
"external_sku": "ERP-1043",
"status": "rejected",
"error": { "code": "validation_error", "message": "title and price_iqd are required to create a new SKU." }
}
]
}A top-level 4xx means the whole request was wrong; a rejected row inside results[] means that one SKU was. Treating "HTTP 200" as "everything synced" is the most common Partner API integration bug.
Status codes#
| Status | Partner API | Checkout API |
|---|---|---|
| 400 | A missing or invalid field, or a batch over the row cap (validation_error); idempotency_key_required when the header is missing. Malformed JSON is refused before it reaches Povito's handlers, in the framework's { "message", "type" } shape rather than this envelope |
validation_error (unknown fields are errors), amount_mismatch, unsupported_currency, idempotency_key_required; otp_invalid, otp_expired on the public surface |
| 401 | Missing, invalid or revoked key (invalid_api_key) — a dashboard login token will not work; a key from the other environment (api_key_wrong_environment) |
invalid_api_key (a publishable key on a merchant route is 401, not 403); invalid_publishable_key on /public/* |
| 403 | The key lacks the scope this endpoint needs (insufficient_scope) |
insufficient_scope, mode_mismatch, merchant_suspended, phone_not_verified, forbidden |
| 404 | The resource does not exist, or external_sku matched none of your products (product_not_found) |
session_not_found, refund_not_found, endpoint_not_found, event_not_found, attempt_not_found, customer_not_found — also for another merchant's or another mode's objects |
| 409 | idempotency_key_conflict (the same key with a different body or on another route), idempotency_key_in_progress (the first request is still running — retry shortly) |
idempotency_key_conflict, session_not_open, attempt_in_progress, refund_exceeds_captured |
| 410 | — | session_expired, session_canceled (public surface) |
| 422 | Only POST /inventory/adjustments: a delta would take stock below zero (validation_error, details[].issue: negative_result), or the seller has no stock location configured |
method_not_enabled, method_not_available, method_not_refundable, return_host_not_allowed, expiry_out_of_range |
| 413 | A request body over 100 KB, refused before Povito's handlers run — split the batch | — |
| 429 | Rate limit exceeded (rate_limited); read Retry-After |
rate_limited, otp_locked; Retry-After is set |
| 5xx | Retryable on Povito's side; log request_id and retry with backoff |
internal_error (500), gateway_error (502), gateway_not_configured (503) |
Retrying safely#
| Partner API | Checkout API | |
|---|---|---|
Idempotency-Key required on |
POST /products/bulk, PATCH /inventory/bulk, POST /inventory/adjustments — 1 to 255 characters, a UUID per logical request |
POST /v1/checkout/sessions, POST /v1/refunds — 8–128 characters of A-Za-z0-9_-; a UUID v4 is the convention |
| Same key sent again | The first outcome, stored for 24 hours against that key and API key, comes back with Idempotent-Replayed: true; nothing runs twice. A different body or another route is 409 idempotency_key_conflict, and a repeat while the first request is still running is 409 idempotency_key_in_progress |
The first response is replayed for 24 hours, with Idempotent-Replayed: true; a different body is 409 idempotency_key_conflict |
| Why a retry is safe | The stored outcome comes back instead of the write running again — including stock adjustments, whose deltas are never applied twice under one key. A request refused before any work started (400, 429, and 404 or 422 on adjustments) stores nothing, so fix it and resend under the same key; a 500 is stored, so check the current state and resend under a new key |
The stored response comes back instead of a second session or refund |
After a 5xx or a network failure |
Retry with backoff | Never assume the operation did not happen: re-send with the same Idempotency-Key, or look the session up by reference_id |