PovitoDevelopers

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-Key with the same body gets the stored response back, with Idempotent-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#

400 — a batch over the row capjson
{
  "error": {
    "code": "validation_error",
    "message": "items cannot exceed 50 rows per request.",
    "details": [{ "field": "items", "issue": "too_many" }],
    "request_id": "req_01J..."
  }
}
  • request_id is on every error; details[] is included only when there is a field to point at.
  • If you send an x-request-id header, its value is used as the request_id.
  • Every code and status: Partner API limits and errors.

Checkout API#

400 — line items do not net to the amountjson
{
  "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_id is on every error and echoed in the x-request-id response header; send your own x-request-id (8–64 characters of A-Za-z0-9_-) to have it used instead.
  • details[] is always present — empty when there is nothing to point at — and each entry names a field (dotted path, (body) for the whole body) and an issue.
  • Every code is enumerated in the contract's ErrorEnvelope schema 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:

200 — one row accepted, one rejectedjson
{
  "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