PovitoDevelopers

Partner API

Povito Partner API v1.0.0

Sync a Povito seller's catalog, stock, prices and orders with the seller's own ERP, POS or inventory system.

Download contract partner-v1.yaml Try requests in the Sandbox explorer

8 operations, generated at build time from the contract. Keys and base URLs for each environment: Keys and environments.

Base URLs#

EnvironmentBase URL
Sandbox Sandbox — accepts only povito_sk_test_ keyshttps://api-staging.povito.com/partner/v1
Live — accepts only povito_sk_live_ keyshttps://api.povito.com/partner/v1

Overview#

Machine-to-machine REST for sellers whose stock lives in their own system. Bulk-first and keyed by the seller's own SKU, so an integration never has to store Povito's internal ids.

Environments#

Environment Base URL Keys accepted
Sandbox https://api-staging.povito.com/partner/v1 povito_sk_test_… only
Live https://api.povito.com/partner/v1 povito_sk_live_… only

A well-formed key presented to the other environment is refused with 401 api_key_wrong_environment before it is looked up, and the message names the key's environment. Nothing a Sandbox integration writes is shown to shoppers.

Keys#

Keys are issued, re-scoped and revoked in the Seller dashboard under Settings → Integrations, never through this API. The full key is shown exactly once, when it is created; Povito stores only a hash of the secret.

Format: povito_sk_{test|live}_{key_id}_{secret}. Both segments can contain _ and -, so treat the whole key as opaque and never split it on _.

A key belongs to exactly one seller and only ever reads or writes that seller's data. Revocation takes effect on the next request.

Scopes#

catalog:read, catalog:write, inventory:write, orders:read, webhooks:manage (reserved — no webhook routes exist yet). Each operation names the one scope it checks; a key without it gets 403 insufficient_scope.

Conventions#

  • Money is integer Iraqi dinars. IQD has no minor units: 25000 is twenty-five thousand dinars.
  • Timestamps are ISO 8601 in UTC.
  • Pagination is forward-only by opaque cursor: pass next_cursor back as cursor until has_more is false. Page on has_more, not on page size.
  • Bulk results are per row. POST /products/bulk and PATCH /inventory/bulk answer 200 even when rows fail; read results[]. A top-level 4xx means the request itself was refused.
  • Idempotency-Key is required on all three write routes. The first outcome is stored for 24 hours against the key and the API key that sent it, and a repeat with the same body gets it back with Idempotent-Replayed: true. The same key with a different body or on another route is 409 idempotency_key_conflict; a repeat while the first request is still running is 409 idempotency_key_in_progress.
  • Errors use one envelope: { "error": { "code", "message", "details"?, "request_id" } }. Quote request_id when you contact support.

Authentication#

partnerApiKey#

HTTP bearerAuthorization: Bearer …

Authorization: Bearer povito_sk_{test|live}_{key_id}_{secret}. The scheme word must be exactly Bearer followed by one space; any other form is treated as no key. No other header is read, and seller dashboard or shopper tokens are always refused. test keys work only in Sandbox and live keys only in Live; the other environment answers 401 api_key_wrong_environment.

Endpoints#

Health

Connectivity probe, no key required.

GET /health#

getHealth · no auth

Check connectivity

Public. Runs no auth middleware, so it answers 200 whatever Authorization header is sent — use it to confirm the host, not the key.

Responses

StatusMeaningBody
200The Partner API is reachable.Health

Examples

Sandboxbash
curl "https://api-staging.povito.com/partner/v1/health"

Catalog

Read and upsert products keyed by the seller's own SKU.

GET /products#

listProducts · partnerApiKey

List the seller's products

Pages of products — every product this seller has an offer on — newest product first (by Povito product id, a stable order).

  • limit counts products. Each product carries all of this seller's variants of it, however many, so a product never appears on two pages.
  • status is applied in the same query that cuts the page, so a page of draft products holds limit of them whenever more exist.

Cursors from before this pagination (offer ids, offer_…) are refused with 400; start again from the first page. There is no updated_since filter; to catch up, re-pull from the first page.

Parameters

NameInTypeNotes
statusqueryProductStatusKeep only products in this status. Unknown values are not rejected; they match nothing.
cursorquerystringThe next_cursor of the previous page; omit for the first page. Opaque. A value that is not a cursor from this list is refused with 400.
limitqueryintegerProducts per page. A value that is not a positive integer is ignored and the default applies; values above 100 are capped at 100.
default 20 · min 1 · max 100
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Responses

StatusMeaningBody
200One page of products.ProductPage
400validation_errorcursor is not a cursor from this list, for example an offer-id cursor from before pages were cut by product (details[]: {field: cursor, issue: invalid}).ErrorEnvelope
401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl "https://api-staging.povito.com/partner/v1/products" \
  -H "Authorization: Bearer povito_sk_test_..."

GET /products/{sku}#

getProductBySku · partnerApiKey

Get one product by the seller's SKU

Resolves the SKU to one of this seller's variants and returns the whole product with all of its variants. Any variant's SKU returns the same product. If the seller has reused one SKU on several products, the first match is returned.

Parameters

NameInTypeNotes
skurequiredpathstringThe seller's own SKU (product_variant.sku), URL-encoded.
min length 1
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Responses

StatusMeaningBody
200The product.Product
401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
404product_not_found — the SKU matches none of this seller's products.ErrorEnvelope
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl "https://api-staging.povito.com/partner/v1/products/{sku}" \
  -H "Authorization: Bearer povito_sk_test_..."

POST /products/bulk#

upsertProductsBulk · partnerApiKey

Create or update up to 50 products

Upsert keyed by external_sku. Rows run concurrently and each gets its own result.

  • SKU already on one of this seller's products → update. Only title, description and category_ids are applied; price_iqd and initial_stock are silently ignored (set price and stock with PATCH /inventory/bulk). Updating a published product moves it back to proposed for review. Catalog-managed products refuse title, description and category changes (catalog_product_locked).
  • New SKU → create. title and price_iqd are required. The product is created as draft with a single Default variant carrying the SKU, initial_stock (default 0) at the seller's first stock location, and the price. This API has no submit-for-review call: drafts stay drafts until submitted from the seller dashboard.

Every row problem is a rejected result, never a 400 for the whole request — including a row that is not an object or has a missing or blank external_sku. Such a row's result has external_sku: null; results are in request order, so its position identifies it. Other fields of the wrong JSON type are dropped rather than rejected. Do not repeat a SKU within one request: rows run concurrently, so two creates for the same new SKU can race.

Idempotent. The first outcome is stored for 24 hours against the Idempotency-Key and the API key that sent it. A repeat with the same body gets the stored status and body back with Idempotent-Replayed: true: the rows do not run again and the rate limit is not spent. The same key with a different body or on another route is 409 idempotency_key_conflict; a repeat while the first request is still running is 409 idempotency_key_in_progress. A request refused before the rows run (400, 429) stores nothing.

Order of checks: key → scope → Idempotency-Key → stored outcome → rate limit → body → rows.

Parameters

NameInTypeNotes
Idempotency-Keyrequiredheaderstring1 to 255 characters; a UUID per logical request is the usual choice. Scoped to the API key that sends it. The first outcome is stored for 24 hours and replayed for a repeat with the same body; see each operation. A longer key is refused with 400 validation_error.
min length 1 · max length 255
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Request body application/json · required

ProductBulkRequest

Responses

StatusMeaningBody
200Per-row results, in request order. Returned even when every row is rejected. With Idempotent-Replayed, the stored results of the earlier request with this key.
header Idempotent-ReplayedIdempotentReplayed
ProductBulkResponse
400The request as a whole was refused, and nothing is stored against its Idempotency-Key. idempotency_key_required when the header is missing; validation_error when the header is longer than 255 characters, or when items is missing, not an array, empty or over the row cap (details[].issue: missing | too_many). A problem with an individual row is never a 400; it is that row's rejected result. Malformed JSON comes back as BodyParserError.
inline schema

One of:

· BulkBadRequest
401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
409idempotency_key_conflict — this API key already used the Idempotency-Key for a different body or operation; send a new key. idempotency_key_in_progress — the first request with this key has not stored its outcome yet; retry shortly. If it persists, check the current state and send a new key.ErrorEnvelope · IdempotencyConflict
413The JSON body is over 100 KB. Split the batch.BodyParserError · PayloadTooLarge
429rate_limited — more than 60 requests to this route with this key in the current 600-second window.
header Retry-AfterRetryAfter
ErrorEnvelope · RateLimited
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl -X POST "https://api-staging.povito.com/partner/v1/products/bulk" \
  -H "Authorization: Bearer povito_sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "external_sku": "ERP-1042",
      "title": "Sony WH-1000XM5 Wireless Headphones",
      "price_iqd": 425000,
      "description": "Noise-cancelling over-ear headphones, black.",
      "category_ids": [
        "pcat_01K3W6H8M2P4R6T8V0X2Z4B6D8"
      ],
      "initial_stock": 12
    },
    {
      "external_sku": "ERP-1043",
      "title": "Anker PowerCore 20000 (renamed)"
    },
    {
      "external_sku": "ERP-1044",
      "title": "USB-C cable 2 m"
    }
  ]
}'

Inventory

Absolute stock and price sync, and audited stock adjustments.

PATCH /inventory/bulk#

updateInventoryBulk · partnerApiKey

Set absolute stock and prices for up to 500 SKUs

The route most integrations call on a schedule. Both fields are absolute values, not deltas, and write no adjustment audit entry (use POST /inventory/adjustments when the reason matters).

  • on_hand sets stocked_quantity at the seller's primary stock location only.
  • price_iqd sets the seller's list price. It works only on a single-variant product; a multi-variant product is rejected with product_has_multiple_variants. During a markdown campaign the discounted price is recomputed from the new list price.

Within a row, on_hand is written before price_iqd. If the price write then fails, the row is reported rejected even though the stock write already happened.

Each row is validated before any of it is written, and a row that fails is rejected with validation_error on its own — the rest of the request still runs:

  • a row that is not an object, or has a missing or blank external_sku (its result has external_sku: null; results are in request order);
  • neither on_hand nor price_iqd set (null counts as not set);
  • on_hand that is not a whole number 0 or more — negative, fractional, or not a JSON number ("12" is rejected, not dropped). Nothing in such a row is written, not even its price_iqd.

A price_iqd of the wrong JSON type is still dropped, and a row whose only field it was reports accepted having changed nothing — send numbers.

Idempotent. The first outcome is stored for 24 hours against the Idempotency-Key and the API key that sent it. A repeat with the same body gets the stored status and body back with Idempotent-Replayed: true: nothing is written again — so a delayed retry cannot overwrite a newer sync with older numbers — and the rate limit is not spent. The same key with a different body or on another route is 409 idempotency_key_conflict; a repeat while the first request is still running is 409 idempotency_key_in_progress. A request refused before the rows run (400, 429) stores nothing. To re-send rows, use a new key.

Order of checks: key → scope → Idempotency-Key → stored outcome → rate limit → body → rows.

Parameters

NameInTypeNotes
Idempotency-Keyrequiredheaderstring1 to 255 characters; a UUID per logical request is the usual choice. Scoped to the API key that sends it. The first outcome is stored for 24 hours and replayed for a repeat with the same body; see each operation. A longer key is refused with 400 validation_error.
min length 1 · max length 255
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Request body application/json · required

InventoryBulkRequest

Responses

StatusMeaningBody
200Per-row results, in request order. Returned even when every row is rejected. With Idempotent-Replayed, the stored results of the earlier request with this key.
header Idempotent-ReplayedIdempotentReplayed
InventoryBulkResponse
400The request as a whole was refused, and nothing is stored against its Idempotency-Key. idempotency_key_required when the header is missing; validation_error when the header is longer than 255 characters, or when items is missing, not an array, empty or over the row cap (details[].issue: missing | too_many). A problem with an individual row is never a 400; it is that row's rejected result. Malformed JSON comes back as BodyParserError.
inline schema

One of:

· BulkBadRequest
401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
409idempotency_key_conflict — this API key already used the Idempotency-Key for a different body or operation; send a new key. idempotency_key_in_progress — the first request with this key has not stored its outcome yet; retry shortly. If it persists, check the current state and send a new key.ErrorEnvelope · IdempotencyConflict
413The JSON body is over 100 KB. Split the batch.BodyParserError · PayloadTooLarge
429rate_limited — more than 60 requests to this route with this key in the current 600-second window.
header Retry-AfterRetryAfter
ErrorEnvelope · RateLimited
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl -X PATCH "https://api-staging.povito.com/partner/v1/inventory/bulk" \
  -H "Authorization: Bearer povito_sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "external_sku": "ERP-1042",
      "on_hand": 12,
      "price_iqd": 419000
    },
    {
      "external_sku": "ERP-1043",
      "on_hand": 0
    },
    {
      "external_sku": "ERP-2001",
      "price_iqd": 35000
    },
    {
      "external_sku": "ERP-2002",
      "on_hand": -3
    }
  ]
}'

POST /inventory/adjustments#

createInventoryAdjustments · partnerApiKey

Post up to 100 stock movements with a reason

Applies signed deltas and writes one audit-trail entry per item. The SKU-keyed counterpart of the seller dashboard's adjustment route.

All or nothing, not per row: an unknown SKU, an unknown location_id, a SKU repeated in the request, or a delta that would take stock below zero refuses the whole request and nothing is written.

category: external_sale with a negative delta and a unit_value posts the outside-sale revenue (and cost of goods, where a unit cost is known) to the seller's books; damaged with a negative delta posts a cost write-off. Bookkeeping is best-effort and never fails the adjustment.

Idempotent. Idempotency-Key is required. The first request's outcome is stored for 24 hours against that key and the API key that sent it. A repeat with the same body gets the stored status and body back with Idempotent-Replayed: true and moves no stock. Bodies are compared as JSON, so member order and whitespace do not matter.

  • The same key with a different body → 409 idempotency_key_conflict.
  • A repeat while the first request is still running → 409 idempotency_key_in_progress; retry shortly. If it persists, the first attempt's outcome is unknown: check stock with GET /products/{sku} and send a new key.
  • A request refused before anything is written — every 400, 404 and 422 below — stores nothing, so the body can be corrected and resent with the same key.
  • An unexpected failure (500) may have come after a write, so it is stored and replayed like a success. Check stock before sending the movement again under a new key.

Order of checks: key → scope → Idempotency-Key → stored outcome → body → SKUs and locations → write. No rate limit applies.

Parameters

NameInTypeNotes
Idempotency-Keyrequiredheaderstring1 to 255 characters; a UUID per logical request is the usual choice. Scoped to the API key that sends it. The first outcome is stored for 24 hours and replayed for a repeat with the same body; see each operation. A longer key is refused with 400 validation_error.
min length 1 · max length 255
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Request body application/json · required

InventoryAdjustmentRequest

Responses

StatusMeaningBody
200Every item was applied — by this request, or by the earlier request with the same Idempotency-Key when Idempotent-Replayed is set.
header Idempotent-ReplayedIdempotentReplayed
InventoryAdjustmentResponse
400idempotency_key_required — no Idempotency-Key header. validation_errorIdempotency-Key longer than 255 characters; reason missing; category not one of the four values; unit_value sent without external_sale or not positive; items empty or over 100; an external_sku missing; a delta that is not a non-zero integer; or the same SKU twice (details[].issue: duplicate_variant).
inline schema

One of:

401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
404inventory_item_not_found — a SKU matches none of this seller's variants, or location_id is not one of this seller's locations.ErrorEnvelope
409idempotency_key_conflict — this API key already used the Idempotency-Key for a different body or operation; send a new key. idempotency_key_in_progress — the first request with this key has not stored its outcome yet; retry shortly. If it persists, check the current state and send a new key.ErrorEnvelope · IdempotencyConflict
413The JSON body is over 100 KB. Split the batch.BodyParserError · PayloadTooLarge
422validation_error — a delta would take stock below zero (details[].issue: negative_result), or the seller has no stock location configured.ErrorEnvelope
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl -X POST "https://api-staging.povito.com/partner/v1/inventory/adjustments" \
  -H "Authorization: Bearer povito_sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Weekly stocktake, Karrada branch",
  "category": "damaged",
  "items": [
    {
      "external_sku": "ERP-1042",
      "delta": -2
    },
    {
      "external_sku": "ERP-1043",
      "delta": 5,
      "location_id": "sloc_01K2F4H6K8M0P2R4T6W8Y0A2C4"
    }
  ]
}'

Orders

Pull the seller's orders. There are no order webhooks and no order actions.

GET /orders#

listOrders · partnerApiKey

List the seller's orders

One row per order placed with this seller (the seller's child order of a multi-seller checkout), newest first. Other sellers' lines on the same checkout are never visible.

Rows are driven by the seller's settlement record for each order: the cursor, since and the row's created_at all refer to when that record was created, which is when the order was placed with this seller.

status is the same seller-facing status GET /orders/{id} reports, so a shipped order reads shipped here too.

Parameters

NameInTypeNotes
sincequerystring (date-time)

Only orders placed with this seller at or after this instant — the filter is on the seller's settlement record's creation time (the row's created_at), not on when the order was last updated. A shipped, cancelled or returned order does not reappear.

An RFC 3339 date-time with a zone: Z or an offset such as +03:00. URL-encode + as %2B, or it arrives as a space. Absent or empty means no lower bound. Anything else — a date without a time, a time without a zone, an impossible date such as 2026-02-30T00:00:00Z, or the parameter sent twice — is refused with 400 validation_error.

cursorquerystringThe next_cursor of the previous page; omit for the first page. Opaque.
limitqueryintegerOrders per page. A value that is not a positive integer is ignored and the default applies; values above 200 are capped at 200.
default 50 · min 1 · max 200
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Responses

StatusMeaningBody
200One page of orders.OrderPage
400validation_errorsince is not an RFC 3339 date-time with a zone (details[]: {field: since, issue: invalid}).ErrorEnvelope
401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl "https://api-staging.povito.com/partner/v1/orders" \
  -H "Authorization: Bearer povito_sk_test_..."

GET /orders/{id}#

getOrder · partnerApiKey

Get one order with its lines and settlement

Every line carries external_sku, the seller's SKU at the time of the order. Accepting, shipping and cancelling happen in the seller dashboard, not here.

Parameters

NameInTypeNotes
idrequiredpathstringThe order_id from GET /orders.
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Responses

StatusMeaningBody
200The order.OrderDetail
401invalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope · Unauthorized
403insufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope · Forbidden
404order_not_found — no such order, or it belongs to another seller.ErrorEnvelope
500internal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope · InternalError

Examples

Sandboxbash
curl "https://api-staging.povito.com/partner/v1/orders/{id}" \
  -H "Authorization: Bearer povito_sk_test_..."

Shared parameters#

IdempotencyKey#

NameInTypeNotes
Idempotency-Keyrequiredheaderstring1 to 255 characters; a UUID per logical request is the usual choice. Scoped to the API key that sends it. The first outcome is stored for 24 hours and replayed for a repeat with the same body; see each operation. A longer key is refused with 400 validation_error.
min length 1 · max length 255

RequestId#

NameInTypeNotes
X-Request-IdheaderstringYour correlation id, echoed as error.request_id on error responses.

Cursor#

NameInTypeNotes
cursorquerystringThe next_cursor of the previous page; omit for the first page. Opaque.

Sku#

NameInTypeNotes
skurequiredpathstringThe seller's own SKU (product_variant.sku), URL-encoded.
min length 1

Response headers#

RetryAfter#

integer — Seconds until the current rate-limit window resets.

IdempotentReplayed#

stringtrue when this response is the stored outcome of an earlier request with the same Idempotency-Key; absent otherwise.

Error responses#

NameMeaningBody
BulkBadRequestThe request as a whole was refused, and nothing is stored against its Idempotency-Key. idempotency_key_required when the header is missing; validation_error when the header is longer than 255 characters, or when items is missing, not an array, empty or over the row cap (details[].issue: missing | too_many). A problem with an individual row is never a 400; it is that row's rejected result. Malformed JSON comes back as BodyParserError.ErrorEnvelope or BodyParserError
Unauthorizedinvalid_api_key — no Authorization: Bearer key, a malformed or unknown key, a wrong secret, or a revoked key. api_key_wrong_environment — a well-formed test key sent to Live or live key sent to Sandbox.ErrorEnvelope
Forbiddeninsufficient_scope — the key is valid but lacks the scope this operation checks.ErrorEnvelope
IdempotencyConflictidempotency_key_conflict — this API key already used the Idempotency-Key for a different body or operation; send a new key. idempotency_key_in_progress — the first request with this key has not stored its outcome yet; retry shortly. If it persists, check the current state and send a new key.ErrorEnvelope
RateLimitedrate_limited — more than 60 requests to this route with this key in the current 600-second window.
header Retry-After
ErrorEnvelope
PayloadTooLargeThe JSON body is over 100 KB. Split the batch.BodyParserError
InternalErrorinternal_error — unexpected server failure. Safe to retry reads with backoff. On the three write routes it is stored against the Idempotency-Key and replayed, because the failure may have come after a write: check the current state, then resend under a new key. Log request_id.ErrorEnvelope

Schemas#

Iqd#

Whole Iraqi dinars. IQD has no minor units.

integer — Whole Iraqi dinars. IQD has no minor units.
e.g. 425000

Timestamp#

string (date-time)
e.g. "2026-09-10T08:12:04.000Z"

ErrorEnvelope#

FieldTypeNotes
errorrequiredobject
fields
FieldTypeNotes
coderequiredErrorCode
messagerequiredstringHuman-readable, English. Do not branch on it; branch on code.
detailsarray of ErrorDetailPresent on some validation_errors; names the offending field.
request_idrequiredstringThe X-Request-Id you sent, or a generated req_… id.

ErrorDetail#

FieldTypeNotes
fieldstring
e.g. "items[3].external_sku"
issuerequiredstringValues used by this API: missing, invalid, too_many, empty, not_applicable, duplicate_variant, negative_result.
e.g. "missing"

ErrorCode#

Codes a top-level error from this API can carry.

string — Codes a top-level error from this API can carry.
one of "validation_error", "idempotency_key_required", "idempotency_key_conflict", "idempotency_key_in_progress", "invalid_api_key", "api_key_wrong_environment", "insufficient_scope", "product_not_found", "inventory_item_not_found", "order_not_found", "rate_limited", "internal_error"

BodyParserError#

Returned by the HTTP framework before Povito's handlers run — malformed JSON or an oversized body. Has no code or request_id.

FieldTypeNotes
messagerequiredstring
typerequiredstring
e.g. "invalid_data"

Health#

FieldTypeNotes
statusrequiredstring
always "ok"
apirequiredstring
always "partner"
versionrequiredstring
always "v1"
currencyrequiredstring
always "IQD"

ProductStatus#

draft (created, not submitted), proposed (awaiting review), published (live), rejected (see rejection_reason).

stringdraft (created, not submitted), proposed (awaiting review), published (live), rejected (see rejection_reason).
one of "draft", "proposed", "published", "rejected"

Product#

FieldTypeNotes
idrequiredstring
e.g. "prod_01K4R8Z2Q6M3VYB7TD1N5HXJ2C"
titlerequiredstring
handlerequiredstring
statusrequiredProductStatus
descriptionrequiredstring | null
thumbnailrequiredstring | null (uri)
imagesrequiredarray of string (uri)
category_idsrequiredarray of string
brandrequiredBrand or null
variantsrequiredarray of VariantEvery variant this seller sells the product as, newest offer first — on GET /products and GET /products/{sku} alike.
catalog_managedrequiredbooleanA Povito catalog product. Its title, description, categories and brand cannot be changed through this API.
handlingrequiredstringCourier handling hint.
one of "none", "fragile", "sealed", "cold"
rejection_reasonrequiredstring | null
created_atrequiredTimestamp
updated_atrequiredTimestamp

Brand#

FieldTypeNotes
idrequiredstring
namerequiredstring
slugrequiredstring

Variant#

One variant as this seller sells it — the seller's offer on that variant.

FieldTypeNotes
offer_idrequiredstring
variant_idrequiredstring
skurequiredstringThe seller's own SKU — the value to use as external_sku.
optionsrequiredmap of stringOption title to value, e.g. {"Size": "M"}. Products created through this API have {"Default": "Default"}.
pricerequiredIqdThe seller's list price. Stays the list price while a markdown runs.
sale_pricerequiredIqd or nullThe discounted price while a Povito markdown campaign runs on this variant, otherwise null.
campaign_labelrequiredstring | null
stocked_quantityrequiredintegerOn-hand units summed across all of this seller's stock locations. Not reserved-adjusted. Always a whole number; a fractional stored level (possible only through writes outside Povito's seller and partner APIs) is rounded to the nearest unit per location.
min 0
conditionrequiredstring | null
notesrequiredstring | null

ProductPage#

FieldTypeNotes
datarequiredarray of Product
next_cursorrequiredstring | null
has_morerequiredboolean

ProductBulkItem#

FieldTypeNotes
external_skurequiredstringTrimmed before use.
min length 1
titlestringRequired when creating.
price_iqdIqdRequired when creating; ignored when the SKU already exists.
descriptionstring
category_idsarray of stringReplaces the product's categories. A catalog-only category rejects the row.
initial_stockintegerUnits at the seller's first stock location when creating; default 0. Ignored when the SKU already exists.

ProductBulkRequest#

FieldTypeNotes
itemsrequiredarray of ProductBulkItem
at least 1 item · at most 50 items

ProductBulkAccepted#

FieldTypeNotes
external_skurequiredstring
statusrequiredstring
always "accepted"
product_idrequiredstring

InventoryBulkAccepted#

FieldTypeNotes
external_skurequiredstring
statusrequiredstring
always "accepted"

BulkRowRejected#

FieldTypeNotes
external_skurequiredstring | nullThe row's trimmed external_sku, or null when the row had none (or was not an object). Results are in request order.
statusrequiredstring
always "rejected"
errorrequiredobjectA single line, never details or request_id.
fields
FieldTypeNotes
coderequiredBulkRowErrorCode
messagerequiredstring

BulkRowErrorCode#

validation_error — the row is not an object or has no external_sku; a create without title/price_iqd; on /inventory/bulk, neither on_hand nor price_iqd set, or an on_hand that is not a whole number 0 or more; or the seller has no stock location. product_not_found / inventory_item_not_found — the SKU is not this seller's. product_has_multiple_variants — price on a multi-variant product. catalog_only_category — a category that accepts only Povito catalog products. catalog_product_locked — editing a catalog product's content. internal_error — anything unexpected; the message is the underlying error's.

stringvalidation_error — the row is not an object or has no external_sku; a create without title/price_iqd; on /inventory/bulk, neither on_hand nor price_iqd set, or an on_hand that is not a whole number 0 or more; or the seller has no stock location. product_not_found / inventory_item_not_found — the SKU is not this seller's. product_has_multiple_variants — price on a multi-variant product. catalog_only_category — a category that accepts only Povito catalog products. catalog_product_locked — editing a catalog product's content. internal_error — anything unexpected; the message is the underlying error's.
one of "validation_error", "product_not_found", "inventory_item_not_found", "product_has_multiple_variants", "catalog_only_category", "catalog_product_locked", "internal_error"

ProductBulkResponse#

FieldTypeNotes
resultsrequiredarray of ProductBulkAccepted or BulkRowRejected

InventoryBulkItem#

One of:

  • any
  • any

InventoryBulkRequest#

FieldTypeNotes
itemsrequiredarray of InventoryBulkItem
at least 1 item · at most 500 items

InventoryBulkResponse#

FieldTypeNotes
resultsrequiredarray of InventoryBulkAccepted or BulkRowRejected

AdjustmentCategory#

string
one of "external_sale", "damaged", "restock", "other"

InventoryAdjustmentItem#

FieldTypeNotes
external_skurequiredstring
min length 1
deltarequiredintegerSigned, non-zero.
location_idstringOne of the seller's stock locations; defaults to the primary location.

InventoryAdjustmentRequest#

FieldTypeNotes
reasonrequiredstringTrimmed; stored on every audit entry.
min length 1
categoryrequiredAdjustmentCategory
unit_valuenumberWhat one unit actually sold for, in IQD. Accepted only with category external_sale.
itemsrequiredarray of InventoryAdjustmentItemEach SKU at most once.
at least 1 item · at most 100 items

InventoryAdjustmentResult#

FieldTypeNotes
variant_idrequiredstring
beforerequiredintegerStocked quantity at the target location before the adjustment, in whole units. A fractional stored level is rounded to the nearest unit, and the adjustment writes after, so it also settles the level to a whole number.
min 0
afterrequiredintegerThe stocked quantity written. Always before + delta.
min 0
deltarequiredinteger

InventoryAdjustmentResponse#

FieldTypeNotes
reasonrequiredstring
categoryrequiredAdjustmentCategory
datarequiredarray of InventoryAdjustmentResult

OrderSummary#

FieldTypeNotes
order_idrequiredstring
increment_idrequiredstring | nullThe shopper-facing checkout number, e.g. POV-100045.
statusrequiredstring | nullIdentical to GET /orders/{id}'s status: shipped once the seller has shipped; otherwise Medusa's status (pending, completed, canceled, archived, …). null if the order record could not be read.
totalrequiredIqdThis seller's order total including shipping. 0 if the order record could not be read.
currencyrequiredstring
always "IQD"
created_atrequiredTimestampWhen the order was placed with this seller (its settlement record's creation time).

OrderPage#

FieldTypeNotes
datarequiredarray of OrderSummary
next_cursorrequiredstring | null
has_morerequiredboolean

OrderLine#

FieldTypeNotes
item_idrequiredstring
titlerequiredstring
quantityrequiredinteger
min 1
unit_pricerequiredIqd
variant_idrequiredstring | null
external_skurequiredstring | null

ShippingAddress#

Missing parts are empty strings, never null.

FieldTypeNotes
first_namerequiredstring
last_namerequiredstring
address_1requiredstring
cityrequiredstring
phonerequiredstring

OrderSettlement#

What the seller will be paid for this order and where that money is.

FieldTypeNotes
statusrequiredstring
one of "pending_capture", "pending_split", "pending_delivery", "hold_active", "released", "refunded", "returned"
commission_percentrequirednumber
commission_amountrequiredIqd
earningsrequiredIqdThe seller's credit for this order after commission.
hold_daysrequiredinteger | null
hold_starts_atrequiredstring | null (date-time)
funds_released_atrequiredstring | null (date-time)
delivery_confirmation_sourcerequiredstring or null
delivery_failed_atrequiredstring | null (date-time)Set when the courier or an admin reported the order will never be delivered.
delivery_failure_reasonrequiredstring | null

OrderDetail#

FieldTypeNotes
order_idrequiredstring
increment_idrequiredstringThe shopper-facing checkout number; falls back to order_id for orders without one.
statusrequiredstringshipped once the seller has shipped; otherwise Medusa's status (pending, completed, canceled, archived, …).
totalrequiredIqdThis seller's order total including shipping.
currencyrequiredstring
always "IQD"
shipping_addressrequiredShippingAddress or null
itemsrequiredarray of OrderLine
accepted_atrequiredstring | null (date-time)
shipped_atrequiredstring | null (date-time)
tracking_numberrequiredstring | null
carrierrequiredstring | null
created_atrequiredTimestamp
settlementrequiredOrderSettlement

Examples#

Product#

A single-variant product created through this API

json
{
  "id": "prod_01K4R8Z2Q6M3VYB7TD1N5HXJ2C",
  "title": "Sony WH-1000XM5 Wireless Headphones",
  "handle": "sony-wh-1000xm5-wireless-headphones",
  "status": "published",
  "description": "Noise-cancelling over-ear headphones, black.",
  "thumbnail": "https://cdn.povito.com/static/products/prod_01K4R8Z2Q6M3VYB7TD1N5HXJ2C/1.jpg",
  "images": [
    "https://cdn.povito.com/static/products/prod_01K4R8Z2Q6M3VYB7TD1N5HXJ2C/1.jpg"
  ],
  "category_ids": [
    "pcat_01K3W6H8M2P4R6T8V0X2Z4B6D8"
  ],
  "brand": {
    "id": "brand_01K3Y2A4C6E8G0J2L4N6Q8S0U2",
    "name": "Sony",
    "slug": "sony"
  },
  "variants": [
    {
      "offer_id": "offer_01K4R8Z3A1B2C3D4E5F6G7H8J9",
      "variant_id": "variant_01K4R8Z2S8T4W6Y8A0C2E4G6J8",
      "sku": "ERP-1042",
      "options": {
        "Default": "Default"
      },
      "price": 425000,
      "sale_price": 382500,
      "campaign_label": "Back to School",
      "stocked_quantity": 12,
      "condition": null,
      "notes": null
    }
  ],
  "catalog_managed": false,
  "handling": "none",
  "rejection_reason": null,
  "created_at": "2026-09-01T10:00:00.000Z",
  "updated_at": "2026-09-04T08:12:00.000Z"
}