OpenShopy
Dokumentacja API

Authentication, Rate Limiting & Conventions

Base URL: https://your-openshopy-domain/api/public/v1

All endpoints except OPTIONS require an API key. The API version is part of the path (v1).

API keys

Two key types exist:

  • Secret keys — os_sk_... — server-side only. Can use every scope granted to the key, including writes and sensitive reads (customers, orders, invoices, discounts, analytics).
  • Publishable keys — os_pk_... — safe for browser/storefront code. They can only be created with the storefront scopes marked Yes in the table below. Endpoints that expose private data return 403 secret_key_required when called with a publishable key, even if the scope matches.

Keys can be revoked (revoked_at) or expire (expires_at); either makes the key invalid.

Sending the key

Send the raw key in one of:

text
Authorization: Bearer <key>

or

text
X-Api-Key: <key>

The Authorization header is checked first (must start with Bearer ); if absent, X-Api-Key is used. If neither is present: 401 unauthenticated.

The server looks up the SHA-256 hash of the key against api_keys.key_hash. If not found, revoked, or expired: 401 invalid_api_key.

Scopes

Every endpoint requires a scope (except GET /orders/track). If the authenticated key's scopes array does not include the route's scope: 403 insufficient_scope.

ScopeAllowsAllowed on publishable keys?
store:readRead store infoYes
products:readRead products, variants, collections, inventory movementsYes
products:writeCreate/update/delete products, variants, images, collectionsNo
inventory:writeAdjust inventory levelsNo
orders:readRead orders, customer order history, abandoned cartsNo
orders:writeCreate/update orders, payments, fulfillments, refunds, returns, notes, notificationsNo
customers:readRead customer recordsNo
customers:writeCreate/update/delete customers, manage addressesNo
discounts:readRead discountsNo
discounts:writeCreate/update/delete discountsNo
shipping:readRead shipping rates/zones/pickup locations/carriersYes
shipping:writeCreate shipping zones/ratesNo
taxes:readRead tax rates, calculate taxYes
taxes:writeUpsert tax ratesNo
invoices:readRead invoicesNo
invoices:writeIssue/correct/credit-note/cancel invoicesNo
checkoutCart & checkout flow, quote, mark-sent-by-token, discount validationYes
customer_accountsStorefront customer auth (register/login/me/addresses/orders/password reset)Yes
analytics:writeIngest analytics eventsYes
analytics:readRead analytics summaryNo

Routes with scope null (no scope check, but still require a valid key): GET /orders/track.

Rate limiting

Each key has a rate_limit_per_minute. Requests are counted per key in one-minute windows (default limit: 120 requests per minute, configurable per key from 10 to 5000).

Response headers on every request (success or error, once a valid key is matched):

text
X-RateLimit-Limit: <key.rate_limit_per_minute>
X-RateLimit-Remaining: <remaining, floored at 0>

When the limit is exceeded (remaining < 0):

http
HTTP/1.1 429
Retry-After: 60
json
{ "error": { "code": "rate_limited", "message": "Too many requests. Slow down and retry after the next minute.", "details": null } }

CORS

All responses (including the 401/403/404 error paths once routing starts) include:

text
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Api-Key, X-Customer-Token, Idempotency-Key
Access-Control-Expose-Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id
Access-Control-Max-Age: 86400

OPTIONS <any path> returns 204 with just the CORS headers (no auth required).

Note: Idempotency-Key is listed as an allowed request header for CORS purposes, but no route in the codebase reads or honors an Idempotency-Key header — there is no idempotency implementation server-side. Retries of POST requests (e.g. order/cart creation) are not deduplicated by the API itself.

Request logging

Every request made with a valid key is logged (visible in the dashboard under API keys → Request log) with: store_id, key_id, method, path (+ truncated query string, 200 chars), status, duration_ms, and ip (from cf-connecting-ip or x-forwarded-for).

Every response also carries X-Request-Id: <uuid>.

Pagination

List endpoints accept:

Query paramTypeDefaultNotes
pageinteger1Minimum 1
limitinteger25Clamped between 1 and 100

Response envelope for paginated endpoints:

json
{
  "data": [ /* array of resources */ ],
  "meta": {
    "page": 1,
    "limit": 25,
    "total": 134,
    "total_pages": 6
  }
}

Non-paginated endpoints return { "data": ... } with no meta, unless a handler sets custom meta (e.g. /taxes/rates includes meta: { prices_include_tax, store_country, destination_based }).

Error format

Every error response is JSON:

json
{
  "error": {
    "code": "not_found",
    "message": "Order not found",
    "details": null
  }
}

Validation errors (zod) return 422 with details as an array of { path, message }:

json
{
  "error": {
    "code": "validation_failed",
    "message": "Request body is invalid",
    "details": [{ "path": "email", "message": "Invalid email" }]
  }
}

Known error codes observed in the code:

HTTP statuscodeWhen
400invalid_jsonBody is not parseable JSON
400invalid_requestMissing required query params (e.g. order tracking)
401unauthenticatedMissing API key
401invalid_api_keyKey not found / revoked / expired
401customer_unauthenticatedMissing/expired X-Customer-Token
401invalid_credentialsBad login/password
403secret_key_requiredEndpoint needs a secret key but a publishable key (or a secret-key-only check) was used
403insufficient_scopeKey lacks the route's required scope
404route_not_foundNo matching path
404not_foundResource not found (Store/Product/Variant/Order/Customer/Invoice/Collection/Image/Shipment/Return/Discount/Zone/Cart)
404store_not_foundAPI key's store missing (shouldn't normally happen)
405method_not_allowedPath matches but method doesn't
409duplicateCustomer with email already exists
409account_existsCustomer account (password) already exists
409cart_convertedCart already checked out
409invalid_discount (409 variant)Discount code unique constraint violated
422validation_failedZod schema failure
422unprocessableGeneric business-rule violation (e.g. "At least one line item is required")
422last_variantAttempt to delete a product's only variant
422invalid_statePayment confirm attempted in wrong state
422order_cancelledAction attempted on a cancelled order
422not_paidRefund attempted on an unpaid order
422checkout_invalidQuote has blocking issues (stock/discount/shipping/product) — details.issues is the full QuoteIssue[] array
422invalid_addressAddress missing line1/city/postal_code
422update_failed / invalid_discountDB update failed validation
422email_required / address_required / shipping_requiredCart checkout preconditions not met
429rate_limitedOver the per-minute limit
500internal_errorUnhandled exception

Money format

All monetary amounts are integers in minor currency units (e.g. cents/grosze) — never floats. Example: 4999 = 49.99 in the order's currency. currency is always one of PLN, USD, EUR. Currency conversion between the store's base currency and the requested currency is done via store.exchange_rates (a flat multiplier map keyed by currency code — not a timestamped rate history).

Tax rates (tax_rates.rate) are decimal percentages, e.g. 23 for 23%, returned as a Number.

Dates

All timestamps are ISO 8601 strings, e.g. "2024-05-01T12:00:00+00:00". Query filters like created_after/created_before/updated_after on /orders accept ISO date/time strings and are passed straight to gte/lt.

Idempotency

Not implemented. The Idempotency-Key header is accepted by CORS but ignored by all handlers. Clients must implement their own deduplication (e.g. checking for an existing cart/order before retrying a POST).