API

API and webhook documentation

Build integrations with NavyFlame: read and write orders, stock, invoices and shipments over a REST API, and receive signed webhooks about events in close to real time.

Introduction

The public NavyFlame API is a server-to-server interface. You authorise every call with an API key, and the responses have a stable, versioned shape. The base address sits on your own tenant domain:

https://{your-slug}.navyflame.com/api/public/v1

The full machine-readable specification (OpenAPI 3.1) is at /openapi-public-v1.yaml and imports into Postman, Insomnia or any OpenAPI client.

Quick start

Start by creating a key in the panel (Account - API) and checking the connection with GET /me - it needs no scope, a valid key is enough. It returns your plan, your limits and the key's scopes.

curl https://{your-slug}.navyflame.com/api/public/v1/me \
  -H "Authorization: Bearer nf_live_your_key"

An example response:

{
  "tenant": "your-slug",
  "keyPrefix": "nf_live_abc123",
  "scopes": ["orders:read", "orders:write"],
  "plan": "Professional",
  "rateLimitPerMin": 3000,
  "apiVersion": "v1"
}

Authentication

You pass the key in the Authorization header as a Bearer token (recommended). For tools without Bearer support, the X-API-Key alias works:

Authorization: Bearer nf_live_your_key
# or
X-API-Key: nf_live_your_key

You create, rotate and revoke keys in the panel. We store only a hash of the key (SHA-256) - you see the full value once, at creation. The key is redacted from every log. Treat it like a password and never put it in the browser.

Scopes

Every key carries scopes. A :write scope includes the matching :read. A missing required scope returns 403 missing_scope.

ScopeGrants access to
orders:readRead orders (list, details, statistics, status history).
orders:writeChange a status, create orders, trigger invoicing.
catalog:readRead the catalogue: products, warehouses, locations, reservations, warehouse documents, bundles.
catalog:writeFull catalogue write: products (CRUD), stock and locations, warehouses, reservations, warehouse documents, bundles.
invoices:readRead invoices and invoice statistics.
shipments:readRead shipments and shipping statistics, plus supporting carrier reads (label PDF, pick-up points, services, dispatch times).
shipments:writeShipment lifecycle: create, buy a carrier label (chargeable at the carrier), cancel, refresh tracking, push a tracking number to Allegro.
shipping:readRead the shipping configuration: rules, zones with rates, delivery options, per-product overrides, plus the cost calculator.
shipping:writeWrite the shipping configuration: rules, zones and per-zone rates, delivery options, per-product shipping overrides.
integrations:readRead the account's connector list (no secrets): key, configuration id, name, enabled and connected state. Never returns credentials.
offers:readRead marketplace offers: per-product product-to-channel mappings, category mappings and live reads from the connector (products, categories, parameters, GPSR entities). Live reads are chargeable (they count towards the connector's API limit) but read-only.
offers:writeWrite marketplace offers: category mappings (create, bulk, redirect, delete, cache sync) plus publishing, synchronising, pulling, refreshing status and importing real offers. Publishing and sync are chargeable (a real call to the marketplace).
reports:readSales, product and operations reports plus CSV/XLSX export. Commercially sensitive data.
analytics:readAI analytics: stock forecasts, price suggestions, anomalies, sales forecast, insights.
monitoring:readIntegration monitoring (webhooks, invoices, error queue) and account usage. The details contain personal data.
monitoring:writeReprocess an entry from the error queue - it fires the original process (for invoices it issues a real invoice; chargeable).
customers:readCustomers (aggregated from orders), statistics, history and CSV export. Full personal data.
email:readE-mail templates and logs. The logs contain recipient addresses and subjects.
email:writeWrite e-mail templates (plan limit) and send messages (chargeable at the provider; a dedicated limit of about 10 an hour).
notifications:readIn-app notifications and preferences.
notifications:writeCreate in-app notifications (the recipient must be a member of the account) and mark them read.
alerts:readAlert rules and the history of firings.
alerts:writeWrite alert rules (an e-mail channel indirectly generates sends).
postsale:readPost-sale handling: messages, disputes, returns, claims (read-only). Buyers' personal data.
postsale:writePost-sale writes with no movement of money: replies in threads and disputes, a dispute decision, accepting or rejecting a return (capability-gated per connector).
postsale:refunds:writeA separate monetary scope: a real refund to the buyer and a commission refund. Split out so that a leaked key without it cannot move money.
templates:readRead description templates and product templates (with variants).
templates:writeWrite description and product templates; applying a template creates a product (also requires catalog:write).
ai:readRead the AI settings and the content generation history.
ai:writeGenerate and translate content with AI (chargeable at your own provider). Writing to a product also requires catalog:write.

Limits per plan

Limits are aggregated per account, not per key. The X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers on every response tell you the state of the window. Once the limit is exceeded you get 429 with a Retry-After header.

PlanAPI keysRequestsWebhooks
Basic51000 / min15
Professional303000 / min50
EnterpriseNo limitNo limitNo limit

Resources and operations

Reads (cursor pagination, filters, sorting):

GET /orders            GET /orders/{id}          GET /orders/stats
GET /orders/{id}/status-history
GET /warehouse-items   GET /warehouse-items/{id}
GET /invoices          GET /invoices/{id}        GET /invoices/stats
GET /shipments         GET /shipments/{id}       GET /shipments/stats

Writes:

PUT   /orders/{id}/status            # change the status (transition matrix)
POST  /orders                        # create an order (source: api)
POST  /orders/{id}/invoice           # trigger invoicing
PATCH /warehouse-items/{id}/stock    # stock correction (set / adjust)

An example: the most recent orders.

curl "https://{your-slug}.navyflame.com/api/public/v1/orders?limit=25" \
  -H "Authorization: Bearer nf_live_your_key"

An example: adjusting stock by -3 pieces (atomically).

curl -X PATCH \
  "https://{your-slug}.navyflame.com/api/public/v1/warehouse-items/{id}/stock" \
  -H "Authorization: Bearer nf_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f9a...-unikat" \
  -d '{"operation":"adjust","quantity":-3}'

Analytics, reports and monitoring

Read-only analytical resources (the reports:read, analytics:read and monitoring:read scopes). Amounts in reports and analytics are numbers (aggregates), unlike the transactional resources, where an amount is text with two decimal places.

GET /reports/sales                 GET /reports/sales/channels
GET /reports/sales/countries       GET /reports/products/bestsellers
GET /reports/products/high-returns GET /reports/products/low-rotation
GET /reports/operations/sla        GET /reports/operations/fulfillment
GET /reports/export/csv            GET /reports/export/xlsx     # binary files

GET /ai-ops/status                 GET /ai-ops/stock-predictions
GET /ai-ops/price-optimizations    GET /ai-ops/anomalies
GET /ai-ops/sales-forecast         GET /ai-ops/insights         # billable (LLM)

GET /monitoring/webhooks           GET /monitoring/webhooks/{id}
GET /monitoring/invoice-links      GET /monitoring/dlq
GET /monitoring/dlq/{id}           GET /monitoring/stats
GET /usage                         # counters, storage used, plan limits

The /reports/export/csv and /xlsx exports are the binary exception to JSON - they return a file with a Content-Disposition: attachment header (CSV in UTF-8 with a BOM, semicolon separator). Errors still come back as application/problem+json.

The details at /monitoring/webhooks/{id} and /monitoring/dlq/{id} contain the connector's raw payload (the buyer's personal data), so the monitoring:read scope is marked sensitive. The list endpoints for those resources return no payload.

An example: a summary of the last 30 days of sales.

curl "https://{your-slug}.navyflame.com/api/public/v1/reports/sales?periodDays=30" \
  -H "Authorization: Bearer nf_live_your_key"

Customers, e-mail, notifications and post-sale handling

Resources containing personal data (the customers:read, email:read, notifications:read, alerts:read and postsale:read scopes). Read-only.

GET /customers                     GET /customers/{email}
GET /customers/stats               GET /customers/{email}/orders
GET /customers/growth              GET /customers/export        # CSV

GET /email-templates(/{id})        GET /email-logs
GET /notifications(/{id})          GET /notifications/unread-count
GET /notification-preferences      # needs ?userId=

GET /alert-rules(/{id})            GET /alert-rules/history

GET /post-sale/capabilities        GET /post-sale/inbox
GET /post-sale/conversations(/{id})(/messages)
GET /post-sale/disputes(/{id})(/messages)
GET /post-sale/returns(/{id})      GET /post-sale/refunds

A customer is a virtual entity (aggregated from orders), addressed by e-mail. Notifications are tenant-wide (an API key does not represent one user); the optional ?userId= narrows them to a recipient, and the preferences require it. Post-sale handling reads only what is stored in the system (no calls to a marketplace at your expense); check availability in /post-sale/capabilities.

The customers:read, email:read and postsale:read scopes expose personal data - grant them only to integrations you trust and process the data in line with the GDPR.

Catalogue, templates and AI

Full catalogue writes (the catalog:read / catalog:write scopes). Creating and irreversible operations require an Idempotency-Key header - repeating with the same key replays the stored result instead of carrying the operation out again.

# Products
POST/PUT/DELETE /warehouse-items(/{id})   POST /warehouse-items/{id}/backorder
GET  /warehouse-items/by-barcode          GET  /warehouse-items/{id}/stock-history
# Warehouses and locations
GET/POST/PUT/DELETE /warehouses(/{id})
GET /warehouse-items/{id}/locations       PUT /warehouse-items/{id}/locations/{warehouseId}
GET/PUT /warehouse-settings
# Reservations and stock documents
GET/POST /reservations                    DELETE /reservations/{id}
GET/POST /stock-documents(/{id})          POST /stock-documents/{id}/lines
POST /stock-documents/{id}/commit         POST /stock-documents/{id}/cancel
# Bundles
GET /bundles(/{id})   PUT /bundles/{id}   DELETE /bundles/{id}

Description and product templates (the templates:read / templates:write scopes). Applying a template creates a product, so POST /product-templates/{id}/apply additionally requires catalog:write.

GET/POST/PUT/DELETE /description-templates(/{id})
GET/POST/PUT/DELETE /product-templates(/{id})
PUT  /product-templates/{id}/variants                  # replace the whole set of variants
POST /product-templates/{id}/variants                  # add a variant
PATCH/DELETE /product-templates/{id}/variants/{variantId}
POST /product-templates/{id}/apply                     # + catalog:write

AI content (the ai:read / ai:write scopes). Generating and translating are chargeable operations at your own AI provider (you configure the key under integrations), which is why Idempotency-Key is required - a repeat replays the result rather than charging for it again. Writing the result into a product (applyToProduct / createNewProducts) additionally requires catalog:write. Bulk operations return a shortened preview per item - the full text comes from /ai/generation-logs or from the product.

GET/PUT /ai/settings                 GET /ai/generation-logs
POST /ai/generate-description        POST /ai/translate
POST /ai/batch-generate              POST /ai/batch-translate     # up to 50 items
POST /ai/preview-template            # no AI; templates:read + catalog:read

An example: creating a description template (idempotently).

curl -X POST \
  "https://{your-slug}.navyflame.com/api/public/v1/description-templates" \
  -H "Authorization: Bearer nf_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1e-tpl-001" \
  -d '{"name":"Premium description","templateBody":"{{productName}} - {{price}} {{currency}}"}'

Shipping configuration

Shipping configuration (the shipping:read / shipping:write scopes): shipping rules, zones with rates, delivery options and per-product overrides, plus the cost calculator. Creating operations require an Idempotency-Key header. In writes, you pass amounts and weight as numbers (not text). This is configuration, not carrier handling - dispatching shipments and labels appear in a separate phase.

# Shipping rules
GET/POST/PUT/DELETE /shipping-rules(/{id})
# Zones and rates (a rate belongs to its owning zone)
GET/POST/PUT/DELETE /shipping-zones(/{id})
POST /shipping-zones/{id}/rates       PUT/DELETE /shipping-zones/{id}/rates/{rateId}
# Delivery options
GET/POST/PUT/DELETE /delivery-options(/{id})
# Per-product shipping overrides
GET/POST /products/{productId}/shipping-overrides
PUT/DELETE /product-shipping-overrides/{id}
# Cost calculator (a POST, but it only needs shipping:read)
POST /shipping/calculate

Names are not unique, so there is no duplicate conflict (409). The deliveryOptionId on a rate or an override must point at your own delivery option (otherwise 422), and a rate outside your zone returns 404. The calculator returns the options sorted by cost, ascending.

An example: working out the shipping cost to Poland.

curl -X POST \
  "https://{your-slug}.navyflame.com/api/public/v1/shipping/calculate" \
  -H "Authorization: Bearer nf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"countryCode":"PL","orderValue":199.99,"totalWeightKg":1.5,"itemCount":2}'

Shipments, labels and carrier

Shipment lifecycle (the shipments:write scope) and supporting carrier reads (the shipments:read scope): you create a shipment, order a label at the carrier, cancel, refresh tracking and push the tracking number to Allegro. You pass weight, dimensions and amounts as numbers.

# Creating a shipment (status Draft; costs nothing)
POST   /shipments                    # one at a time
POST   /shipments/bulk               # in bulk, up to 100 (partial success)
# The carrier label
POST   /shipments/{id}/label         # BILLABLE - buying a label (Idempotency-Key required)
GET    /shipments/{id}/label         # fetch the label (PDF, binary)
# The rest of the lifecycle
DELETE /shipments/{id}               # delete a draft or cancel with the carrier
POST   /shipments/{id}/refresh-tracking
POST   /shipments/{id}/push-tracking
# Helper reads (shipments:read)
GET    /shipments/points                       # pickup points (InPost, Poczta Polska, Furgonetka)
GET    /couriers/{provider}/services           # carrier services (Apaczka, Furgonetka)
GET    /shipments/allegro/delivery-services    # Allegro Delivery services
POST   /shipments/available-dates              # pickup dates (DHL; a POST, but shipments:read)

Ordering a label is chargeable - it buys a real shipment at the carrier. That is why POST /shipments/{id}/label requires an Idempotency-Key header: repeating with the same key replays the stored result and never buys a second label (the protection sits on the server). You can order a label only for a shipment in Draft. Downloading the label (GET .../label) is the binary exception - it returns application/pdf, not JSON.

A carrier or Allegro connector that is not connected returns 403 not_supported (never a 500), and an error on the carrier's or Allegro's side returns 502 bad_gateway. Supporting reads for an unsupported provider return { "supported": false } rather than an error. A cancellation does not change the local status if the carrier rejects the operation.

An example: ordering a label (idempotently, chargeable).

curl -X POST \
  "https://{your-slug}.navyflame.com/api/public/v1/shipments/{id}/label" \
  -H "Authorization: Bearer nf_live_your_key" \
  -H "Idempotency-Key: 6f9a...-label-001"

Connectors and marketplace offers

Connector discovery (the integrations:read scope) plus reads and writes of marketplace offers (the offers:read / offers:write scopes). Some operations are local (from your own data, at no cost) and some are live, chargeable calls to the connector - marked below.

Discovery and local reads (at no cost):

GET /connectors                          # the list of connectors (no secrets)
GET /warehouse-items/{id}/listings       # the product's marketplace offers (keyset)
GET /category-mappings                   # category mappings (keyset)

GET /connectors returns a strictly limited set of fields - key, configId, displayName, enabled and connected - and never returns credentials (no API keys, tokens or connector passwords). The connected field is a computed state: the connector is enabled and has credentials stored. You pass the configId value as the ?sourceConfigId= or ?destConfigId= filter in category mappings.

A product's offers (/warehouse-items/{id}/listings) tie a stock item to an offer in a channel (the external id, the price and status on the marketplace side, and any per-channel price override). Every offer object carries an instanceId, the durable id of one particular connection, and an instanceName to show the user. That way the same product can have independent offers, prices and synchronisation state on, say, ebay.de and ebay.com. The lastSyncError field is scrubbed of credential patterns a connector error might contain.

An example: the account's connectors.

curl "https://{your-slug}.navyflame.com/api/public/v1/connectors" \
  -H "Authorization: Bearer nf_live_your_key"

Live marketplace reads (the offers:read scope) make a real call to the connector - they are chargeable (they count towards the connector's API limit) but strictly read-only (they neither publish nor change an offer). A connector that is not connected returns 403 not_supported (never 500), an error on the marketplace side returns 502 bad_gateway. The shape of the data depends on the connector.

GET /marketplace/{connectorKey}/products
GET /marketplace/{connectorKey}/products/{externalProductId}
GET /marketplace/{connectorKey}/categories?q=fraza
GET /marketplace/{connectorKey}/categories/{categoryId}/parameters
GET /marketplace/{connectorKey}/responsible-producers
GET /marketplace/{connectorKey}/responsible-persons

Writing category mappings (the offers:write scope) are local writes - none of them publishes or changes a real offer visible to a buyer (publishing a real offer is described below). POST /category-mappings requires an Idempotency-Key header (a duplicate gives 409); sync refreshes the local category cache from the connector (chargeable, but idempotent).

POST   /category-mappings           # create (Idempotency-Key required; a duplicate -> 409)
POST   /category-mappings/bulk      # in bulk (up to 1000; duplicates skipped)
PUT    /category-mappings/{id}       # repoint the mapping at another target
DELETE /category-mappings/{id}       # delete the mapping
POST   /category-mappings/sync      # refresh the local category cache (billable, idempotent)

Publishing and synchronising real offers (the offers:write scope) creates or updates an offer visible to buyers in the channel - chargeable operations (a real call to the marketplace). publish and sync require an Idempotency-Key header (a retry with the same key replays the stored response instead of repeating a chargeable call). publish returns 409 when the product is already published on that particular connection (or its publication is in progress) - an offer on a different instance of the same connector causes no conflict. Use sync for that same configId instead. A connector that is not connected gives 403 not_supported, an error on the marketplace side gives 502 bad_gateway.

In the body of publish, sync and PUT .../pricing, pass the configId you got from GET /connectors. If a connector has several enabled connections, omitting the field ends in 422 validation_failed. The API does not pick a default account blindly. Where exactly one enabled connection exists, it can resolve it automatically.

Important on a 502 during publication: a timeout or a 5xx from the marketplace is ambiguous - the offer may have been created. A retry with the same Idempotency-Key replays the same 502 (it does not publish again). Verify the offer in the channel and only retry with a new Idempotency-Key once you know the offer was not created.

POST /warehouse-items/{id}/publish/{connectorKey}          # publish the offer (billable; Idempotency-Key required)
POST /warehouse-items/{id}/sync/{connectorKey}             # update the offer (billable; Idempotency-Key required)
POST /warehouse-items/{id}/pull/{connectorKey}             # pull the offer from the channel into the catalogue
POST /warehouse-items/{id}/refresh-status/{connectorKey}   # refresh the status of one offer
POST /warehouse-items/refresh-status                       # in bulk (up to 100 products)
PUT  /warehouse-items/{id}/listings/{connectorKey}/pricing # price per instance (local, no outbound call)

# Body for publish / sync:
{ "configId": "8d1ec28d-..." }

# Body for pricing:
{ "configId": "8d1ec28d-...", "mode": "markup", "value": 12.5 }

Importing an offer into stock ( POST /catalog/import) creates a stock item from a marketplace product. It requires two scopes at once - offers:write and catalog:write (missing either gives 403 missing_scope) - plus an Idempotency-Key header. A duplicate SKU or barcode gives 409 conflict.

POST /catalog/import   # import a marketplace product -> a stock item (offers:write + catalog:write)

Writes: notifications, alerts, e-mail and post-sale handling

Writes for communication, reprocessing and post-sale handling (the notifications:write, alerts:write, email:write, monitoring:write, postsale:write and postsale:refunds:write scopes). Creating and irreversible operations require an Idempotency-Key header; naturally idempotent operations (marking read, toggling) honour it optionally.

# Notifications (notifications:write)
POST /notifications                  # create (the recipient must be a member of the account; Idempotency-Key required)
POST /notifications/{id}/read        # mark one as read
POST /notifications/read-all         # mark all (needs ?userId=)
# Alerts (alerts:write)
POST /alert-rules                    # create a rule (Idempotency-Key required)
PUT/DELETE /alert-rules/{id}         # update / delete
POST /alert-rules/{id}/toggle        # enable / disable
# E-mail (email:write)
POST/PUT/DELETE /email-templates(/{id})   # templates (plan limit; a duplicate name -> 409)
POST /emails/send                    # BILLABLE - a real send (Idempotency-Key required)

Creating a notification checks that recipientUserId is a member of the account (a non-member gives 422, an uncertain check gives 503) - an API key must not be able to flood any user's inbox. E-mail templates are subject to the plan limit (Basic 10, Professional 24, Enterprise unlimited), giving 403 plan_limit, and a duplicate name gives 409. Sending e-mail (`POST /emails/send`) is chargeable and covered by a dedicated limit of about 10 an hour per account ( 429); a provider with no configuration gives 403 not_supported, a transport error gives 502 bad_gateway. An alert rule with the email or both channel indirectly generates sends when it fires.

Reprocessing the error queue (the monitoring:write scope) fires the original process of a queue entry - for invoicing sources it issues a real invoice, so it is chargeable and destructive, and Idempotency-Key is required. Only three sources are retriable (shopify_to_wfirma_pipeline, shopify, wfirma_invoice) - others give 422; an unavailable Temporal gives 503. A repeat is idempotent by a deterministic process identifier (it returns alreadyRunning).

POST /monitoring/dlq/{id}/retry      # BILLABLE - runs the original process (Idempotency-Key required)

Post-sale handling (the postsale:write scope): you reply in threads and disputes, change a dispute's status, and accept or reject a return. Ownership of the thread, dispute or return is checked before the channel is called (someone else's or a non-existent identifier gives 404), and the operations are capability-gated per connector - a missing capability or method returns 403 not_supported (never 500). A channel error gives 502 bad_gateway.

# Post-sale handling with no movement of money (postsale:write)
POST /post-sale/conversations/{id}/messages   # reply to the buyer (Idempotency-Key required)
POST /post-sale/conversations/{id}/read        # mark the thread read
POST /post-sale/disputes/{id}/messages         # reply in a dispute (Idempotency-Key required)
POST /post-sale/disputes/{id}/status           # a decision in a dispute (see money isolation)
POST /post-sale/returns/{id}/accept            # accept the return
POST /post-sale/returns/{id}/reject            # reject the return
# Refunds - a SEPARATE money scope (postsale:refunds:write)
POST /post-sale/refunds/issue                  # a REAL refund to the buyer
POST /post-sale/refunds/commission-claim       # a commission refund

Money is isolated. A refund has its own `postsale:refunds:write` scope, split off from postsale:write, so that a leaked key without it cannot move money. The POST /post-sale/disputes/{id}/status route belongs to postsale:write, but if the decision moves money to the buyer (a partialRefund present, or the ACCEPTED_REFUND / ACCEPTED_PARTIAL_REFUND status), the key must additionally carry `postsale:refunds:write` - otherwise the request returns 403 missing_scope and nothing is carried out.

Retrying refunds. refunds/issue and commission-claim are idempotent on the channel side (your Idempotency-Key becomes the operation's identifier at the channel) - on a 502, retry with the same key and the channel will deduplicate. A dispute decision that moves money, however, has no idempotency at the channel - on an uncertain error, verify the outcome in the sales channel and only retry with a new Idempotency-Key (the same 502 is returned for the same key).

An example: a real refund (idempotently, chargeable).

curl -X POST \
  "https://{your-slug}.navyflame.com/api/public/v1/post-sale/refunds/issue" \
  -H "Authorization: Bearer nf_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f9a...-refund-001" \
  -d '{"connectorKey":"shopify","instanceId":"shop-1","orderExternalId":"1234","amount":49.99,"currency":"PLN"}'

The full list of operations (per resource)

A complete, navigable index of every public v1 endpoint grouped by resource. This is the layer for a human - the full field-level request and response schemas (179 of them) are in the OpenAPI specification . Every living endpoint is listed here.

  • Scope - the scope the key needs. A :write scope includes the matching :read. The notation a + b means cross-scope (both are needed at once).
  • Idem. - K! = the Idempotency-Key header is required; K? = honoured (optional); blank = not applicable (a read).
  • Errors - the key codes beyond the universal 401, 403 and 429 (plus 500/503, which can occur anywhere).

Meta

OperationScopeIdem.PurposeErrors
GET /me(brak)-Key introspection: plan, limits, scopes. Needs no scope.-

Orders

OperationScopeIdem.PurposeErrors
GET /ordersorders:read-List orders (keyset; filters for status / source / date / search).422
POST /ordersorders:writeK!Create an order (source=api); the server computes the amounts.409,413,415,422
GET /orders/{id}orders:read-Order details.404
PUT /orders/{id}/statusorders:writeK?Change the status (transition matrix; writeback to Allegro).404,415,422
POST /orders/{id}/invoiceorders:writeK!Trigger invoicing (asynchronous).404,409,422
GET /orders/statsorders:read-Order statistics (counters per status plus Paid revenue).422
GET /orders/{id}/status-historyorders:read-History of an order's real status transitions.404,422

Catalogue: products

OperationScopeIdem.PurposeErrors
GET /warehouse-itemscatalog:read-List products (keyset; filters for status / sku / barcode).422
POST /warehouse-itemscatalog:writeK!Create a warehouse product.409,413,415,422
GET /warehouse-items/{id}catalog:read-Product details (with variants).404
PUT /warehouse-items/{id}catalog:writeK!Update a product.404,409,413,415,422
DELETE /warehouse-items/{id}catalog:writeK!Archive a product (soft delete).404,409,422
PATCH /warehouse-items/{id}/stockcatalog:writeK?Set or adjust stock (set / adjust, atomically).404,415,422
GET /warehouse-items/by-barcodecatalog:read-Find a product or variant by barcode.404,422
GET /warehouse-items/{id}/stock-historycatalog:read-History of a product's stock changes (keyset).404,422
POST /warehouse-items/{id}/backordercatalog:writeK!Turn a product's backorder on or off.404,409,415,422

Catalogue: warehouses and locations

OperationScopeIdem.PurposeErrors
GET /warehousescatalog:read-List warehouses / locations.-
POST /warehousescatalog:writeK!Create a warehouse / location.409,415,422
PUT /warehouses/{id}catalog:writeK!Update a warehouse.404,409,415,422
DELETE /warehouses/{id}catalog:writeK!Delete a warehouse.404,409,422
GET /warehouse-items/{id}/locationscatalog:read-A product's stock broken down by location.404
PUT /warehouse-items/{id}/locations/{warehouseId}catalog:writeK!Set a product's stock at a given location.404,409,415,422
GET /warehouse-settingscatalog:read-Warehouse automation settings.-
PUT /warehouse-settingscatalog:writeK!Change the warehouse automation settings.409,415,422

Catalogue: reservations and warehouse documents

OperationScopeIdem.PurposeErrors
GET /reservationscatalog:read-List stock reservations (keyset).422
POST /reservationscatalog:writeK!Reserve a product's stock.404,409,415,422
DELETE /reservations/{id}catalog:writeK!Release a reservation.404,409
GET /stock-documentscatalog:read-List warehouse documents (keyset).422
POST /stock-documentscatalog:writeK!Create a warehouse document (draft).409,415,422
GET /stock-documents/{id}catalog:read-Document details (with lines).404
POST /stock-documents/{id}/linescatalog:writeK!Add a line to a document.404,409,415,422
DELETE /stock-documents/{id}/lines/{lineId}catalog:writeK!Remove a line from a document.404,409,422
POST /stock-documents/{id}/commitcatalog:writeK!Commit a document (apply the stock change).404,409,422
POST /stock-documents/{id}/cancelcatalog:writeK!Cancel a warehouse document.404,409,422

Catalogue: bundles

OperationScopeIdem.PurposeErrors
GET /bundlescatalog:read-List bundles (keyset).422
GET /bundles/{id}catalog:read-Bundle details (components plus computed stock).404
PUT /bundles/{id}catalog:writeK!Set a bundle's components.404,409,415,422
DELETE /bundles/{id}catalog:writeK!Delete a bundle (clear its components).404,409,422

Templates (descriptions and products)

OperationScopeIdem.PurposeErrors
GET /description-templatestemplates:read-List description templates (keyset).422
POST /description-templatestemplates:writeK!Create a description template.409,413,415,422
GET /description-templates/{id}templates:read-Description template details.404
PUT /description-templates/{id}templates:writeK?Update a description template.404,409,413,415,422
DELETE /description-templates/{id}templates:writeK?Delete a description template.404
GET /product-templatestemplates:read-List product templates (keyset).422
POST /product-templatestemplates:writeK!Create a product template.409,413,415,422
GET /product-templates/{id}templates:read-Product template details (with variants).404
PUT /product-templates/{id}templates:writeK?Update a product template.404,409,413,415,422
DELETE /product-templates/{id}templates:writeK?Delete a product template.404
PUT /product-templates/{id}/variantstemplates:writeK?Replace a template's whole set of variants (bulk).404,409,413,415,422
POST /product-templates/{id}/variantstemplates:writeK!Add a variant to a template.404,409,415,422
PATCH /product-templates/{id}/variants/{variantId}templates:writeK?Update a template variant.404,409,415,422
DELETE /product-templates/{id}/variants/{variantId}templates:writeK?Delete a template variant.404
POST /product-templates/{id}/applytemplates:write + catalog:writeK!Create a product from a template (cross-scope).404,409,422

AI: content generation

OperationScopeIdem.PurposeErrors
GET /ai/settingsai:read-Global AI generation settings.-
PUT /ai/settingsai:writeK?Change the global AI settings.409,415,422
GET /ai/generation-logsai:read-History of AI calls (keyset).422
POST /ai/generate-descriptionai:writeK!Generate a product description (billable).404,409,415,422
POST /ai/translateai:writeK!Translate a product (billable).404,409,415,422
POST /ai/batch-generateai:writeK!Generate descriptions in bulk (billable; max 50).409,413,415,422
POST /ai/batch-translateai:writeK!Translate in bulk (billable; max 50).409,413,415,422
POST /ai/preview-templatetemplates:read + catalog:read-Preview a template with product data (no LLM; cross-scope).404,413,415,422

AI analytics (AI-Ops)

OperationScopeIdem.PurposeErrors
GET /ai-ops/statusanalytics:read-AI configuration status plus data coverage.-
GET /ai-ops/stock-predictionsanalytics:read-Stock run-out forecast per SKU (cached 5 min).422
GET /ai-ops/price-optimizationsanalytics:read-Price suggestions from the price change history (cached 5 min).422
GET /ai-ops/anomaliesanalytics:read-Anomalies against the rolling average (cached 5 min).422
GET /ai-ops/sales-forecastanalytics:read-Sales forecast (regression; days = horizon).422
GET /ai-ops/insightsanalytics:read-Insights (billable, uses the tenant's LLM; cached 30 min; rule-based fallback).-

Invoices

OperationScopeIdem.PurposeErrors
GET /invoicesinvoices:read-List invoices (keyset; issueDate desc).422
GET /invoices/{id}invoices:read-Invoice details (with lines and the tax-authority record).404
GET /invoices/statsinvoices:read-Invoice statistics (lifetime KPIs).-

Shipments and carrier

OperationScopeIdem.PurposeErrors
GET /shipmentsshipments:read-List shipments (keyset).422
GET /shipments/{id}shipments:read-Shipment details (with tracking history).404
GET /shipments/statsshipments:read-Shipment statistics (counters per status).-
POST /shipmentsshipments:writeK!Create a shipment (Draft status; no cost).409,415,422
POST /shipments/bulkshipments:writeK!Create shipments in bulk (max 100, partial success).409,413,415,422
POST /shipments/{id}/labelshipments:writeK!Order a carrier label (billable - a real cost).404,409
GET /shipments/{id}/labelshipments:read-Download the carrier label (PDF, binary).404,409,502
DELETE /shipments/{id}shipments:writeK!Delete an unsent draft or cancel a shipment at the carrier.404,409,502
POST /shipments/{id}/refresh-trackingshipments:writeK?Refresh the status and tracking at the carrier.404,409,502
POST /shipments/{id}/push-trackingshipments:writeK!Push the tracking number to the order in the marketplace it came from (Allegro, TikTok Shop).404,400,502
POST /shipments/{id}/push-to-allegroshipments:writeK!The same, but for Allegro only. Kept for backwards compatibility - use push-tracking in new integrations.404,409,422,502
POST /shipments/available-datesshipments:read-Available dispatch dates (DHL; POST-but-read).415,422,502
GET /shipments/pointsshipments:read-Pick-up points / parcel lockers (InPost, Poczta Polska, Furgonetka; the service parameter narrows Furgonetka results to that service's carrier).502
GET /couriers/{provider}/servicesshipments:read-List a carrier's services (Apaczka, Furgonetka).502
GET /shipments/allegro/delivery-servicesshipments:read-List Allegro Delivery services.502

Shipping configuration

OperationScopeIdem.PurposeErrors
GET /shipping-rulesshipping:read-List shipping rules (keyset).422
POST /shipping-rulesshipping:writeK!Create a shipping rule.409,415,422
GET /shipping-rules/{id}shipping:read-Shipping rule details.404
PUT /shipping-rules/{id}shipping:writeK?Update a shipping rule.404,409,415,422
DELETE /shipping-rules/{id}shipping:writeK?Delete a shipping rule.404
GET /shipping-zonesshipping:read-List shipping zones (with nested rates).422
POST /shipping-zonesshipping:writeK!Create a shipping zone.409,415,422
GET /shipping-zones/{id}shipping:read-Zone details (with nested rates).404
PUT /shipping-zones/{id}shipping:writeK?Update a shipping zone.404,409,415,422
DELETE /shipping-zones/{id}shipping:writeK?Delete a shipping zone.404
POST /shipping-zones/{id}/ratesshipping:writeK!Add a rate to a zone.404,409,415,422
PUT /shipping-zones/{id}/rates/{rateId}shipping:writeK?Update a rate in a zone.404,409,415,422
DELETE /shipping-zones/{id}/rates/{rateId}shipping:writeK?Delete a rate from a zone.404
GET /delivery-optionsshipping:read-List delivery options (keyset).422
POST /delivery-optionsshipping:writeK!Create a delivery option.409,415,422
GET /delivery-options/{id}shipping:read-Delivery option details.404
PUT /delivery-options/{id}shipping:writeK?Update a delivery option.404,409,415,422
DELETE /delivery-options/{id}shipping:writeK?Delete a delivery option.404
GET /products/{productId}/shipping-overridesshipping:read-Shipping overrides for a product (keyset).404,422
POST /products/{productId}/shipping-overridesshipping:writeK!Create a shipping override for a product.404,409,415,422
PUT /product-shipping-overrides/{id}shipping:writeK?Update a product's shipping override.404,409,415,422
DELETE /product-shipping-overrides/{id}shipping:writeK?Delete a product's shipping override.404
POST /shipping/calculateshipping:read-Work out the shipping options and cost (POST-but-read).415,422

Customers

OperationScopeIdem.PurposeErrors
GET /customerscustomers:read-List customers (aggregated by e-mail; keyset).422
GET /customers/statscustomers:read-Customer statistics (KPIs).-
GET /customers/growthcustomers:read-Customer growth over time (a monthly series).422
GET /customers/exportcustomers:read-Export customers to CSV (binary).422
GET /customers/{email}customers:read-Customer details (aggregate plus addresses).404
GET /customers/{email}/orderscustomers:read-A customer's orders (keyset).404,422

E-mail

OperationScopeIdem.PurposeErrors
GET /email-templatesemail:read-List e-mail templates (keyset).422
GET /email-templates/{id}email:read-E-mail template details.404
GET /email-logsemail:read-List the logs of e-mails sent (keyset; personal data).422
POST /email-templatesemail:writeK!Create an e-mail template (plan limit).409,413,415,422
PUT /email-templates/{id}email:writeK?Update an e-mail template.404,409,413,415,422
DELETE /email-templates/{id}email:writeK?Delete an e-mail template.404,409
POST /emails/sendemail:writeK!Send an e-mail message (billable; about 10 an hour).409,413,415,422,502

Notifications

OperationScopeIdem.PurposeErrors
GET /notificationsnotifications:read-List tenant-wide notifications (keyset; ?userId narrows it).422
GET /notifications/{id}notifications:read-Notification details.404
GET /notifications/unread-countnotifications:read-The number of unread notifications.422
GET /notification-preferencesnotifications:read-Notification preferences (requires ?userId=).422
POST /notificationsnotifications:writeK!Create a notification (the recipient must be a member of the account).413,415,422
POST /notifications/{id}/readnotifications:writeK?Mark a notification read.404,409
POST /notifications/read-allnotifications:writeK?Mark all read (requires ?userId=).409,422

Alerts

OperationScopeIdem.PurposeErrors
GET /alert-rulesalerts:read-List alert rules (keyset).422
GET /alert-rules/historyalerts:read-History of alert firings (keyset).422
GET /alert-rules/{id}alerts:read-Alert rule details.404
POST /alert-rulesalerts:writeK!Create an alert rule.413,415,422
PUT /alert-rules/{id}alerts:writeK?Update an alert rule.404,413,415,422
DELETE /alert-rules/{id}alerts:writeK?Delete an alert rule.404,409
POST /alert-rules/{id}/togglealerts:writeK?Turn an alert rule on or off.404,409,422

Post-sale handling

OperationScopeIdem.PurposeErrors
GET /post-sale/capabilitiespostsale:read-A map of post-sale capabilities per connector instance.-
GET /post-sale/inboxpostsale:read-The aggregate inbox (a summary of threads and disputes).-
GET /post-sale/conversationspostsale:read-List threads (keyset).422
GET /post-sale/conversations/{id}postsale:read-Thread details.404
GET /post-sale/conversations/{id}/messagespostsale:read-Messages in a thread (keyset).404,422
GET /post-sale/disputespostsale:read-List disputes / complaints (keyset).422
GET /post-sale/disputes/{id}postsale:read-Dispute details.404
GET /post-sale/disputes/{id}/messagespostsale:read-Messages in a dispute (keyset).404,422
GET /post-sale/returnspostsale:read-List returns (keyset).422
GET /post-sale/returns/{id}postsale:read-Return details.404
GET /post-sale/refundspostsale:read-List refunds (keyset).422
POST /post-sale/conversations/{id}/messagespostsale:writeK!Reply to the buyer in a thread.404,413,415,422,502
POST /post-sale/conversations/{id}/readpostsale:writeK?Mark a thread read.404,409
POST /post-sale/disputes/{id}/messagespostsale:writeK!Reply in a dispute.404,413,415,422,502
POST /post-sale/disputes/{id}/statuspostsale:writeK!Change a dispute's status (moving money requires refunds:write).404,413,415,422,502
POST /post-sale/returns/{id}/acceptpostsale:writeK!Accept a return.404,409,502
POST /post-sale/returns/{id}/rejectpostsale:writeK!Reject a return.404,413,415,422,502

Refunds (an isolated monetary scope)

OperationScopeIdem.PurposeErrors
POST /post-sale/refunds/issuepostsale:refunds:writeK!A real refund to the buyer (idempotent per channel).413,415,422,502
POST /post-sale/refunds/commission-claimpostsale:refunds:writeK!Claim a commission refund.413,415,422,502

Reports

OperationScopeIdem.PurposeErrors
GET /reports/salesreports:read-Sales report (a time series plus aggregates and margin).422
GET /reports/sales/channelsreports:read-Sales split by channel.422
GET /reports/sales/countriesreports:read-Sales split by country.422
GET /reports/products/bestsellersreports:read-Bestsellers (sales plus gross margin).422
GET /reports/products/high-returnsreports:read-Products with a high return rate.422
GET /reports/products/low-rotationreports:read-Products with low rotation.422
GET /reports/operations/slareports:read-Fulfilment SLA metrics.422
GET /reports/operations/fulfillmentreports:read-Fulfilment time per channel.422
GET /reports/export/csvreports:read-Export a report to CSV (binary).422
GET /reports/export/xlsxreports:read-Export a report to XLSX (binary).422

Monitoring and usage

OperationScopeIdem.PurposeErrors
GET /monitoring/webhooksmonitoring:read-List incoming webhooks (no payload).422
GET /monitoring/webhooks/{id}monitoring:read-Webhook detail (raw payload - personal data).404
GET /monitoring/invoice-linksmonitoring:read-Invoicing links (order to invoice).422
GET /monitoring/dlqmonitoring:read-The error queue (dead-letter, no payload).422
GET /monitoring/dlq/{id}monitoring:read-An error queue entry (the original payload - personal data).404
GET /monitoring/statsmonitoring:read-Aggregate integration statistics.-
GET /usagemonitoring:read-Account usage (counters, storage, plan limits).-
POST /monitoring/dlq/{id}/retrymonitoring:writeK!Retry an error queue entry (billable; fires the original workflow).404,409,422

Connectors and marketplace offers

OperationScopeIdem.PurposeErrors
GET /connectorsintegrations:read-List the tenant's connectors (a projection with no secrets).-
GET /warehouse-items/{id}/listingsoffers:read-One product's marketplace offers, separately for each instance (keyset).404,422
GET /category-mappingsoffers:read-List category mappings (keyset).422
POST /category-mappingsoffers:writeK!Create a category mapping (locally).409,413,415,422
POST /category-mappings/bulkoffers:writeK?Create mappings in bulk (max 1000).409,413,415,422
PUT /category-mappings/{id}offers:writeK?Redirect a mapping to a different target.404,409,413,415,422
DELETE /category-mappings/{id}offers:writeK?Delete a category mapping.404,409
POST /category-mappings/syncoffers:writeK?Refresh the local category cache (billable).409,413,415,422,502
GET /marketplace/{connectorKey}/productsoffers:read-A live list of products from the connector (billable).422,502
GET /marketplace/{connectorKey}/products/{externalProductId}offers:read-A live single product from the connector (billable).502
GET /marketplace/{connectorKey}/categoriesoffers:read-A live search of the connector's categories (billable).502
GET /marketplace/{connectorKey}/categories/{categoryId}/parametersoffers:read-Live category parameters (billable).502
GET /marketplace/{connectorKey}/responsible-producersoffers:read-A live list of GPSR manufacturers (billable).502
GET /marketplace/{connectorKey}/responsible-personsoffers:read-A live list of GPSR responsible persons (billable).502
POST /warehouse-items/{id}/publish/{connectorKey}offers:writeK!Publish an offer on the instance named by configId (billable, a real offer).404,409,413,415,422,502
POST /warehouse-items/{id}/sync/{connectorKey}offers:writeK!Update an offer on the instance named by configId (billable).404,409,413,415,422,502
POST /warehouse-items/{id}/pull/{connectorKey}offers:writeK?Pull an offer from the connector into the catalogue (billable).404,409,502
POST /warehouse-items/{id}/refresh-status/{connectorKey}offers:writeK?Refresh the status of one offer (billable).404,409,502
POST /warehouse-items/refresh-statusoffers:writeK?Refresh the status of offers in bulk (max 100).409,413,415,422
PUT /warehouse-items/{id}/listings/{connectorKey}/pricingoffers:writeK?Set the price for a specific instance from configId (locally, with no call).404,409,413,415,422
POST /catalog/importoffers:write + catalog:writeK!Import a marketplace product into stock (cross-scope).409,413,415,422

Pagination and filters

Lists are paginated by cursor (no offset). The response carries pagination.nextCursor - pass it in the cursor parameter to fetch the next page. The updatedSince parameter (an ISO date-time with a zone) returns records changed since that moment. For pull synchronisation, poll with an overlapping window and deduplicate by id.

GET /orders?limit=50&cursor=eyJjIjoi...&updatedSince=2026-07-01T00:00:00Z

Idempotency

Write operations are protected by the Idempotency-Key header (any unique value, a UUID is recommended, up to 255 characters). It is required on POST /orders and POST /orders/{id}/invoice, and honoured on PUT and PATCH.

  • A repeat with the same key and the same body returns the stored result (the Idempotency-Replayed: true header) with no double effect.
  • The same key with a different body returns 409 idempotency_conflict.
  • A parallel duplicate still being processed returns 409 idempotency_in_progress with Retry-After.
  • Keys expire after 24 hours.

Code examples

Ready-made snippets in three flavours: curl, JavaScript (fetch, Node 20+) and Python (the requests library). The JS and Python variants assume the BASE (base address) and KEY (the key from secure storage) constants set in the authentication example.

Authentication (Bearer and X-API-Key)

curl

# Bearer (recommended)
curl https://{your-slug}.navyflame.com/api/public/v1/me \
  -H "Authorization: Bearer nf_live_your_key"

# The X-API-Key alias (tools without Bearer support)
curl https://{your-slug}.navyflame.com/api/public/v1/me \
  -H "X-API-Key: nf_live_your_key"

JavaScript

const BASE = "https://my-shop.navyflame.com/api/public/v1";
const KEY = process.env.NAVYFLAME_API_KEY;

const res = await fetch(BASE + "/me", {
  headers: { Authorization: "Bearer " + KEY },
});
if (!res.ok) throw new Error("HTTP " + res.status);
const me = await res.json();
console.log(me.plan, me.scopes);

Python

import os, requests

BASE = "https://my-shop.navyflame.com/api/public/v1"
KEY = os.environ["NAVYFLAME_API_KEY"]

r = requests.get(BASE + "/me", headers={"Authorization": "Bearer " + KEY})
r.raise_for_status()
me = r.json()
print(me["plan"], me["scopes"])

Cursor pagination (a loop plus deduplication)

curl

curl "https://{your-slug}.navyflame.com/api/public/v1/orders?limit=100&updatedSince=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer nf_live_your_key"
# the response carries pagination.nextCursor -> pass it as &cursor=... on the next call

JavaScript

async function* paginate(path) {
  let cursor = null;
  do {
    const url = new URL(BASE + path);
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);
    const res = await fetch(url, {
      headers: { Authorization: "Bearer " + KEY },
    });
    if (res.status === 429) {
      const wait = Number(res.headers.get("Retry-After") || 1);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error("HTTP " + res.status);
    const page = await res.json();
    for (const row of page.data) yield row;
    cursor = page.pagination.nextCursor;
  } while (cursor);
}

const seen = new Set();
for await (const order of paginate("/orders?updatedSince=2026-07-01T00:00:00Z")) {
  if (seen.has(order.id)) continue; // deduplicate by id
  seen.add(order.id);
  // ... process the order
}

Python

import time

def paginate(path, params=None):
    params = dict(params or {})
    params["limit"] = 100
    while True:
        r = requests.get(BASE + path, params=params,
                         headers={"Authorization": "Bearer " + KEY})
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue
        r.raise_for_status()
        page = r.json()
        for row in page["data"]:
            yield row
        cursor = page["pagination"]["nextCursor"]
        if not cursor:
            break
        params["cursor"] = cursor

seen = set()
for order in paginate("/orders", {"updatedSince": "2026-07-01T00:00:00Z"}):
    if order["id"] in seen:  # deduplicate by id
        continue
    seen.add(order["id"])
    # ... process the order

An idempotent write (Idempotency-Key plus backoff)

curl

curl -X POST "https://{your-slug}.navyflame.com/api/public/v1/orders" \
  -H "Authorization: Bearer nf_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c9a20-7b2e-4c6d-9f10-000000000001" \
  -d '{"customer":{"name":"Jan Kowalski"},"lineItems":[{"title":"Kubek 300 ml","sku":"KUB-300-BIA","quantity":2,"unitPrice":"39.99","taxRate":"23"}],"currency":"PLN"}'

JavaScript

import { randomUUID } from "node:crypto";

async function createOrder(body) {
  const key = randomUUID(); // the same key on every retry
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(BASE + "/orders", {
      method: "POST",
      headers: {
        Authorization: "Bearer " + KEY,
        "Content-Type": "application/json",
        "Idempotency-Key": key,
      },
      body: JSON.stringify(body),
    });
    if (res.status === 429 || res.status === 502 || res.status === 503) {
      const wait = Number(res.headers.get("Retry-After") || 2 ** attempt);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue; // transient - retry with the same key
    }
    if (res.status === 409) {
      const problem = await res.json();
      if (problem.code === "idempotency_in_progress") {
        await new Promise((r) => setTimeout(r, 1000));
        continue;
      }
      throw new Error("idempotency conflict: " + problem.code); // a different body
    }
    if (!res.ok) throw new Error("HTTP " + res.status);
    return res.json(); // 201 or replayed (Idempotency-Replayed: true)
  }
  throw new Error("wyczerpano proby ponowienia");
}

Python

import time
from uuid import uuid4

def create_order(body):
    key = str(uuid4())  # the same key on every retry
    for attempt in range(5):
        r = requests.post(BASE + "/orders", json=body, headers={
            "Authorization": "Bearer " + KEY,
            "Idempotency-Key": key,
        })
        if r.status_code in (429, 502, 503):
            time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
            continue  # transient - retry with the same key
        if r.status_code == 409:
            code = r.json().get("code")
            if code == "idempotency_in_progress":
                time.sleep(int(r.headers.get("Retry-After", "1")))
                continue
            raise RuntimeError("idempotency conflict: " + str(code))
        r.raise_for_status()
        return r.json()  # 201 or replayed
    raise RuntimeError("wyczerpano proby ponowienia")

Webhook signature verification (JavaScript and Python) is in the Outgoing webhooks section.

Error format

We return errors following RFC 9457 (application/problem+json). Each carries a stable code and a requestId for correlating with support.

{
  "type": "https://navyflame.com/dokumentacja/api/bledy#validation_failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "Invalid order input.",
  "code": "validation_failed",
  "requestId": "req_abc123",
  "errors": [{ "field": "lineItems[0].unitPrice", "message": "Kwota >= 0 (jako string)." }]
}
CodeHTTPWhenWhat to do
unauthorized401A missing, invalid, expired or revoked API key.Check the Authorization / X-API-Key header. If the key was revoked or has expired, create a new one in the panel.
missing_scope403The key lacks a required scope (for example orders:write), including cross-scope cases where an operation needs two.Grant the missing scope by rotating the key with the full set of scopes. An existing key's scopes cannot be changed in place.
plan_limit403The plan does not include the API, the plan limit is exhausted, or the subscription has expired or been cancelled.Renew or upgrade the plan. The API returns automatically after activation (the limit propagates within about 5 minutes).
not_supported403The operation needs a carrier or marketplace connector that is not connected (capability gate; never a 500).Connect and enable the right connector in the panel (Integrations). Check availability in /post-sale/capabilities or /connectors.
not_found404The resource does not exist in your account (or belongs to another tenant, which is indistinguishable, anti-IDOR).Verify the identifier. Someone else's identifier always looks non-existent.
conflict409A resource state conflict: the offer is already published, a duplicate SKU, barcode or name, no stock, or a document not in Draft.Resolve the conflict: use sync instead of publish; change the SKU or name; check the resource's status.
idempotency_conflict409The same Idempotency-Key used with a different request body.Use a new Idempotency-Key for a new body. The same key must carry an identical body.
idempotency_in_progress409A request with this key is being processed right now (a parallel duplicate).Wait as the Retry-After header says and repeat with the same key.
payload_too_large413The request body exceeds 2 MB.Reduce the body (for example smaller batches in bulk operations).
unsupported_media_type415A write without the Content-Type: application/json header.Set Content-Type: application/json on write operations.
validation_failed422Bad input: a wrong value, a forbidden status transition, a bad cursor, a naive datetime with no time zone.Correct the data following the errors field in the response (field plus message).
rate_limited429A limit was exceeded (the plan's per-account limit, per-IP, in-flight, or the dedicated e-mail sending limit).Slow down, respect Retry-After and apply exponential backoff. Check the X-RateLimit-* headers.
internal500An error on the NavyFlame side.Retry with backoff. If it keeps happening, report the requestId to support.
bad_gateway502An error at an external service (a carrier, Allegro or another marketplace) - treat it as transient.Retry with backoff. On a write, retry with the same Idempotency-Key (the channel will deduplicate). See the notes on publishing and refunds.
service_unavailable503A restart, a deployment, or a configuration not ready yet (continuous deployment).Retry with backoff, this is a transient state.

The 500 (internal) and 503 (service_unavailable) codes can occur on any operation and are not declared per endpoint. Deployments are continuous - a client should treat occasional 502/503 as transient (retry with backoff) and protect writes with an Idempotency-Key header. The type field in problem+json points at this section's anchor (for example #validation_failed).

Outgoing webhooks

You subscribe to an event and give an HTTPS address, and we send a signed notification there. The v1 events are order.synced, order.status_changed, invoice.issued, shipment.status_updated and catalog.updated. Every delivery carries the X-NavyFlame-Event, X-NavyFlame-Delivery-Id and X-NavyFlame-Signature headers. Success is a 2xx response within 5 seconds; on failure we retry with a growing interval for about 24 hours, and you can see the delivery history in the panel.

The shape of the payload (the common envelope):

{
  "id": "evt_9f4c2f6a-...",
  "type": "order.synced",
  "apiVersion": "v1",
  "occurredAt": "2026-07-07T12:00:00Z",
  "tenant": "your-slug",
  "origin": { "channel": "api", "apiKeyPrefix": "nf_live_abc123" },
  "data": { "order": { }, "isNew": true }
}

The origin.channel field (api | panel | sync | automation) lets you break an echo loop - if you made the change yourself with an API key, you will recognise it by apiKeyPrefix and skip the event. The receiver deduplicates by the event id and by the X-NavyFlame-Delivery-Id header (at-least-once delivery, with no ordering guarantee).

The event catalogue (v1)

A closed list of v1 events - new types are an additive change, so a receiver must ignore unknown fields and types. Every event carries the common envelope (above); the type-specific content is in the data field:

order.synced - After an order is saved, only on a real change of data (a new order or a status transition). The isNew field distinguishes new from updated.

{
  "id": "evt_9f4c2f6a-1b2c-4d3e-8a9b-000000000001",
  "type": "order.synced",
  "apiVersion": "v1",
  "occurredAt": "2026-07-09T12:00:00Z",
  "tenant": "my-shop",
  "origin": { "channel": "sync" },
  "data": {
    "order": { "id": "0e6b7c...", "orderNumber": "2026/07/000123",
               "status": "Processing", "totalPrice": "129.99", "currency": "PLN" },
    "isNew": false
  }
}

order.status_changed - After a confirmed save of a real status change, whatever the source. Setting the same status again, or a rejected transition, sends no event. The order field holds the state after the save, previousStatus the status before it, and origin points at the source of the change.

{
  "id": "evt_2b3c4d5e-6f70-4a8b-9c0d-000000000005",
  "type": "order.status_changed",
  "apiVersion": "v1",
  "occurredAt": "2026-08-30T10:30:00Z",
  "tenant": "my-shop",
  "origin": { "channel": "api", "apiKeyPrefix": "nf_live_abc123" },
  "data": {
    "order": { "id": "0e6b7c...", "orderNumber": "2026/08/000321",
               "status": "Processing", "totalPrice": "129.99", "currency": "PLN" },
    "previousStatus": "New"
  }
}

invoice.issued - After an invoice is issued in the tenant's accounting system.

{
  "id": "evt_1a2b3c4d-5e6f-4a7b-8c9d-000000000002",
  "type": "invoice.issued",
  "apiVersion": "v1",
  "occurredAt": "2026-07-09T12:03:00Z",
  "tenant": "my-shop",
  "origin": { "channel": "automation" },
  "data": {
    "invoice": { "id": "7a1c9e...", "number": "FV/2026/07/45",
                 "status": "Issued", "grossAmount": "129.99", "currency": "PLN" }
  }
}

shipment.status_updated - After a shipment's status changes at the carrier. The previousStatus field carries the earlier status (or null).

{
  "id": "evt_2b3c4d5e-6f7a-4b8c-9d0e-000000000003",
  "type": "shipment.status_updated",
  "apiVersion": "v1",
  "occurredAt": "2026-07-09T12:10:00Z",
  "tenant": "my-shop",
  "origin": { "channel": "sync" },
  "data": {
    "shipment": { "id": "b2d9f1...", "courierProvider": "inpost",
                  "status": "InTransit", "trackingNumber": "6800000000001" },
    "previousStatus": "PickedUp"
  }
}

catalog.updated - After a product or a stock level changes. Single changes come per item; explicitly bulk operations (a wholesaler import, a bulk call) come as one aggregate event (bulk: true).

{
  "id": "evt_3c4d5e6f-7a8b-4c9d-0e1f-000000000004",
  "type": "catalog.updated",
  "apiVersion": "v1",
  "occurredAt": "2026-07-09T12:15:00Z",
  "tenant": "my-shop",
  "origin": { "channel": "api", "apiKeyPrefix": "nf_live_abc123" },
  "data": {
    "warehouseItemId": "c4e1a2...", "sku": "KUB-300-BIA",
    "changeType": "updated", "stockQuantity": 42
  }
}
// The bulk variant (data):
// { "bulk": true, "source": "wholesale-import", "itemsChanged": 128 }

Signature verification

The X-NavyFlame-Signature header has the form t=<unix>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>." + raw_body). The header may carry several v1= pairs (a secret rotation window - the old and the new secret sign in parallel for 24 h) - accept any that matches. Check that the timestamp agrees (a tolerance of 300 s) to protect yourself against a replay.

JavaScript (Node)

import crypto from "node:crypto";

function verify(signatureHeader, rawBody, secret) {
  const parts = Object.create(null);
  const sigs = [];
  for (const p of String(signatureHeader).split(",")) {
    const [k, v] = p.split("=");
    if (k === "t") parts.t = v;
    if (k === "v1" && v) sigs.push(v);
  }
  if (!parts.t || sigs.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const computed = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");
  return sigs.some(
    (s) =>
      s.length === computed.length &&
      crypto.timingSafeEqual(
        Buffer.from(computed, "hex"),
        Buffer.from(s, "hex"),
      ),
  );
}

Python

import hashlib, hmac, time

def verify(signature_header, raw_body, secret):
    t = None
    sigs = []
    for part in signature_header.split(","):
        k, _, v = part.partition("=")
        if k == "t":
            t = v
        elif k == "v1" and v:
            sigs.append(v)
    if not t or not sigs:
        return False
    if abs(time.time() - int(t)) > 300:  # replay protection
        return False
    computed = hmac.new(
        secret.encode(), (t + "." + raw_body).encode(), hashlib.sha256
    ).hexdigest()
    return any(hmac.compare_digest(computed, s) for s in sigs)

Verify the signature against the raw request body (the bytes before JSON parsing) - serialising again changes the bytes and the signature will not match. raw_body / rawBody are exactly the bytes received in the POST body, before JSON decoding.

A recipe for Zapier and Make

  1. In Zapier, create a Zap with the Webhooks by Zapier - Catch Hook trigger (in Make: the Custom Webhook module). Copy the address it generates.
  2. In the NavyFlame panel (Account - API - Webhooks) add a subscription: choose the event and paste the address as the target. You see the signing secret once, so save it.
  3. Use the Send test button in the panel to confirm your scenario receives the payload.
  4. (Recommended) Add a signature verification step using the function above before you trust the data.

GDPR

Webhook payloads carry buyers' personal data. When you configure a subscription (choosing the destination address, for example Zapier, usually a transfer outside the EEA) you act as the controller of that data, and the subscription is your documented processing instruction. Propagating a buyer's deletion or anonymisation into systems fed by the API and webhooks is on your side.

Guides

Quick start (from a key to the first call)

  1. In the panel (Account - API) create a key with the scopes you need. You see the key only once, so save it in a secure secret store.
  2. Confirm the connection: GET /me (needs no scope). It returns the plan, the limits and the key's scopes.
  3. Make the first call against the resource you want, for example GET /orders?limit=25.

Pull synchronisation (updatedSince plus an overlap window plus dedupe)

  1. Poll with updatedSince = last_sync - overlap_window (5 minutes, say). The semantics are inclusive (>=), with millisecond precision.
  2. Page through by cursor until pagination.nextCursor is null.
  3. Deduplicate by id - >= alone does not protect you from a record whose transaction committed after your read (hence the overlap window).
  4. Save the highest updatedAt from the page as the starting point for the next run.
  5. After an account is restored from a backup, do a full resync - do not rely on updatedSince (and create new keys).

Zapier and Make (a webhook into an action)

  1. In Zapier, create the Webhooks by Zapier - Catch Hook trigger (in Make: Custom Webhook) and copy the address it generates.
  2. In the panel (Account - API - Webhooks) add a subscription: choose the event, paste the address as the target, save the secret (shown once).
  3. Click Send test in the panel to confirm the payload arrives.
  4. Add a signature verification step (see the Webhooks section) before you trust the data.
  5. For calls back into the API, use the HTTP / Custom Request module with an Authorization: Bearer header.

Rotating a key and a webhook secret (a 24 h window)

  • API key: rotating in the panel returns a new token once; the old one keeps working for 24 h (no downtime) - swap it in your integration within that window. A revoke takes effect immediately.
  • Webhook secret: the rotation has an overlap window - the old and the new secret sign in parallel for 24 h. The signature header can carry several v1= pairs, so accepting any that matches makes rotation a non-breaking change.
  • After a restore from a backup the API keys are revoked automatically - create new ones and do a full resync.

Handling limits (Retry-After plus backoff)

  • Read the plan limit from GET /me ( rateLimitPerMin) or GET /usage. Limits are aggregated per account, not per key.
  • Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset - steer your pace as you go.
  • On a 429, respect Retry-After (seconds) and apply exponential backoff. Keep concurrency below the in-flight limit (20 parallel requests per account).
  • Enterprise has the per-minute limit switched off; the per-IP flood guard and the concurrency cap always apply (they protect the infrastructure, they are not a plan feature).

Versioning and changes

The version is in the path (/api/public/v1). Additive changes (new fields, new endpoints, new event types) do not break compatibility - your client must ignore unknown fields. We announce breaking changes in advance: a changelog entry, an e-mail to the owners of active keys, and the Deprecation and Sunset headers.

Changelog

The v1 version history (the version number is the next phase of the build-out). Every change is additive - we have removed nothing and changed the meaning of no endpoint or field. The current OpenAPI specification version: v1.10.0.

VersionPhaseWhat was added (additively)
v1.0GAThe API core: /me, orders (read, write and invoicing), stock (read plus a stock PATCH), invoices (read), shipments (read). Webhooks: order.synced, invoice.issued, shipment.status_updated, catalog.updated. Keyset pagination, idempotency, RFC 9457 errors.
v1.1Phase ARead-only analytics: sales, product and operations reports (plus CSV/XLSX export), AI-Ops, monitoring, usage, and order statistics and status history.
v1.2Phase BRead-only personal data: customers, e-mail templates and logs, notifications and preferences, alert rules, post-sale handling.
v1.3Phase CCatalogue writes: products (CRUD), warehouses and locations, reservations, warehouse documents, bundles, templates (descriptions and products) and AI content (billable).
v1.4Phase D1Shipping configuration writes: rules, zones with rates, delivery options, per-product overrides and the cost calculator.
v1.5Phase D2Shipment lifecycle and carrier: creation (single and bulk), buying a label (billable), cancellation, tracking, push to Allegro, and supporting carrier reads (label PDF, points, services, times).
v1.6Phase E1Connector discovery (no secrets) plus local reads of marketplace offers and category mappings.
v1.7Phase E2Live, chargeable marketplace reads (products, categories, GPSR) plus writing category mappings (and cache synchronisation).
v1.8Phase E3Publishing, synchronising, pulling, importing and per-channel pricing of real marketplace offers (billable).
v1.9Phase FCommunication and post-sale writes: notifications, alerts, e-mail (billable sending), reprocessing the error queue, and post-sale handling and refunds (the isolated postsale:refunds:write scope).
v1.10WebhooksA new order.status_changed event after a confirmed real change of an order's status, with previousStatus and origin. No event for a no-op or a rejected transition.

Versioning is in the path: a breaking change would go to /v2, with v1 supported alongside it for at least 6 months from the announcement (a changelog entry, an e-mail to the owners of active keys, and the Deprecation and Sunset headers).

Good practice

  • Test the connection against GET /me before your first integration.
  • Always attach an Idempotency-Key to write operations and retry transient errors with backoff.
  • After an account is restored from a backup, do a full resynchronisation (do not rely on updatedSince) and create new keys - the old ones are invalidated by the restore.
  • For pull synchronisation, use updatedSince with an overlap window and deduplicate by id.

Frequently asked questions

Sign in to the panel, go to Account - API and create a key with the scopes you need. You see the key only once, so save it somewhere safe. Make your first test against GET /me, to confirm the connection and see your plan and limits.

An API key is for incoming calls: your system asks NavyFlame for data or carries out an operation. A webhook is an outgoing notification: NavyFlame sends you a signal about an event (a new order, say) as soon as it happens.

To receive webhooks, use the Webhooks module (Zapier) or Custom Webhook (Make) and give the address as the subscription target in the panel. For API calls, use the HTTP / Custom Request module with an Authorization: Bearer header. Signature verification is described in the Webhooks section.

Not if you use the Idempotency-Key header. Repeating a request with the same key returns the same result with no double effect. The header is required when creating orders and triggering invoicing.

The API returns 403 with the plan_limit code until it is renewed. After an account is restored from a backup, API keys are invalidated automatically (create new ones) and we recommend a full resynchronisation rather than relying on updatedSince.

Ready to connect your own system?

Create an account, make an API key in the panel and start with GET /me.

Create an account

This site uses cookies

We use cookies to keep the site working, to measure traffic and to personalise content. Read more in our privacy policy.

Manage preferences