Skip to content

Authentication and errors ​

API keys ​

Every request carries an API key in the Authorization header:

http
GET /api/v1/products HTTP/1.1
Host: zerpy.app
Authorization: Bearer sk_live_9f2c…
  • Who creates keys: only an owner or admin, in Integrations → API keys → New API key. Give the key a name so you know where it is used, choose its access (the Webshop preset = all four scopes, Product feed (read only) = products:read + stock:read) and click Create key.
  • Shown once: the full key is displayed only right after creation. ZERPY stores only a SHA-256 hash of it — a lost key cannot be recovered; create a new one.
  • Revoking: Revoke in the key list. Systems using the key stop working immediately (401 invalid_api_key).
  • The key list shows when each key was last used.
  • Key prefixes: sk_live_ = company key, sk_feed_ = partner feed key created by a reseller in the partner portal.

WARNING

Keep keys on your server. Anyone with the key can read your catalog and stock — and, with the order scopes, create and cancel orders.

Only GET /feed.csv also accepts the key as a query parameter (?key=…), because spreadsheet and shop importers can only fetch a URL. Every other endpoint needs the header.

Scopes ​

ScopeUI labelAllows
products:readRead products and pricesGET /products, GET /products/{sku}, GET /feed.csv
stock:readRead stock levelsGET /stock
orders:readRead orders and shipment trackingGET /orders, GET /orders/{id}
orders:writeCreate and cancel ordersPOST /orders, POST /orders/{id}/cancel, POST /orders/{id}/payment-link

A request with a key that lacks the scope gets 403 insufficient_scope. Partner feed keys always have exactly products:read + stock:read, whatever else is stored.

Rate limit ​

120 requests per minute per key (a token bucket that refills continuously). Each successful authentication returns X-RateLimit-Remaining. Above the limit:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 1

{ "error": { "code": "rate_limited", "message": "Too many requests (limit 120/minute)" } }

Wait Retry-After seconds and retry. For bulk syncs use the largest page size (limit=200 for products, limit=500 for stock) instead of one call per product.

Errors ​

Every error has the same shape:

json
{
  "error": {
    "code": "unknown_sku",
    "message": "Unknown SKU: ABC",
    "details": { "skus": ["ABC"] }
  }
}

code is stable and machine-readable; message is for humans and may change. details is present only for some codes.

HTTPcodeWhen
400invalid_amountPayment link: amount is more than the open balance
401missing_api_keyNo Authorization: Bearer … header (and no ?key= on feed.csv)
401invalid_api_keyUnknown, revoked or expired key; partner feed key of an archived partner
403insufficient_scopeThe key lacks the scope; partner key used on an order endpoint
404not_foundProduct or order does not exist (or belongs to another company)
409cannot_cancelOrder is already shipped, has an invoice, or is already cancelled
409cannot_payPayment link: Montonio not connected, order cancelled, currency not EUR / PLN, nothing left to pay
409no_warehousePOST /orders: the company has no own warehouse
422validation_failedBody or query is invalid — details is a list of { "path", "message" }
422unknown_skuPOST /orders: a SKU does not exist or is archived (details.skus)
422unsupported_currencyPOST /orders: currency is not the company currency
422options_requiredPOST /orders: a required option group of a configurable product is not answered (details.sku, details.groups)
422invalid_optionsPOST /orders: unknown option value, or two values of one group (details.sku)
429rate_limitedRate limit exceeded
500internal_errorSomething went wrong on our side — retry later
503provider_unavailablePayment link: Montonio did not answer

Example validation error:

json
{
  "error": {
    "code": "validation_failed",
    "message": "Request is invalid",
    "details": [
      { "path": "customer.email", "message": "Invalid email address" },
      { "path": "lines", "message": "Too small: expected array to have >=1 items" }
    ]
  }
}

Pagination ​

List endpoints take page (from 1) and limit and return:

json
{ "data": [ … ], "page": 1, "limit": 100, "total": 342, "has_more": true }
EndpointDefault limitMax limitSort order
GET /products100200SKU; by updated_at when updated_since is given
GET /stock500500SKU
GET /orders50100updated_at (oldest first)

Keep requesting page + 1 while has_more is true.

Incremental sync ​

GET /products and GET /orders accept updated_since (ISO 8601 timestamp with time zone, e.g. 2026-11-07T10:00:00Z) and return only records changed after it, oldest change first. Store the updated_at of the last record you processed and use it as the next updated_since.

Idempotency ​

POST /orders is idempotent on external_id (your shop's order number, unique per company):

  • Sending an order whose external_id already exists does not create a second order. ZERPY returns the existing order with status 200 and the header Idempotent-Replayed: true (and empty warnings).
  • This also holds for two identical requests arriving at the same moment.
  • So it is safe to retry POST /orders after a timeout or a network error.

POST /orders/{id}/payment-link reuses an unpaid link for the same amount (200) instead of creating a new one (201). POST /orders/{id}/cancel on an already cancelled order returns 409 cannot_cancel.