Webhooks and API
Connect your client's store to an ERP, an external warehouse or Zapier. An API key gives the other system precisely scoped access to the Admin API, and a webhook sends it a signed message whenever something happens in the store.
API key
- Store staff create the key in the admin under Shop › Integrations, choose its scopes and copy it. It is shown only once; the database keeps only its hash. A revoked key stops working with the next request.
- A key belongs to one market of the store. Requests go to the Admin API (GraphQL, POST /admin-api) with the key in the
vendure-api-keyheader and the market token in thevendure-tokenheader; the same screen shows the address and the token. Without the market token the key is not valid. - A key may only run the operations of its scopes from the table below and the
activeChannelquery; anything else returns FORBIDDEN. No scope allows payments, refunds, order cancellation, settings or permissions. - Each key is limited to 300 requests per minute. Above that the response is 429 with a Retry-After header.
curl https://admin.portican.com/admin-api \
-H 'content-type: application/json' \
-H 'vendure-api-key: <API key>' \
-H 'vendure-token: <market token>' \
-d '{"query":"{ orders(options: { take: 5 }) { items { code state totalWithTax } } }"}'
Key scopes
From the platform code. This list is built directly from the platform source code, and every deployment checks that the two still match.
| Scope | What it allows |
|---|---|
Read ordersobjednavky-citanie | Orders with items and totals, including the customer details in them (name, delivery address).order, orders |
Fulfilmentexpedicia | Read orders, create shipments and change their state. The tracking number comes from the label printed by store staff, not from the key.order, orders, fulfillmentHandlers, addFulfillmentToOrder, transitionFulfillmentToState |
Read cataloguekatalog-citanie | Products, variants with stock, collections and facets.product, products, productVariant, productVariants, collection, collections, facet, facets |
Edit cataloguekatalog-zapis | Change products and variants, including price and stock, without deleting anything.product, products, productVariant, productVariants, collection, collections, facet, facets, updateProduct, updateProductVariants |
Read customerszakaznici-citanie | Customers with their contact details and addresses.customer, customers |
Webhooks
- Staff add a webhook on the same screen: an address and the events. The address must be https on a public server on the standard port, without a user name or password; an address in a private network is rejected. A market can have at most 10 webhooks.
- Every message is a POST with a JSON body and the headers
portican-podpis,portican-udalost,portican-id-udalosti,portican-pokus. The Send test event button sends askuskaevent. - A message carries identifiers, state, amounts and items, never the customer's name, e-mail, phone or address. The receiving system reads the details with an API key whose scope the merchant granted.
Events
From the platform code. This list is built directly from the platform source code, and every deployment checks that the two still match.
| Event | When it is sent |
|---|---|
objednavka.prijata | The customer completed an order (also when paying by bank transfer or cash on delivery). |
objednavka.zaplatena | The order is fully paid: card payment, bank transfer marked as received, or cash on delivery after delivery. |
objednavka.odoslana | A shipment left, for the whole order or a part of it (field stav). |
objednavka.dorucena | A shipment was delivered, for the whole order or a part of it (field stav). |
objednavka.zrusena | A placed order was cancelled. |
zasoba.zmenena | The stock or the reserved quantity of a variant changed: restocking, sale, reservation in an order, cancellation. |
zakaznik.novy | A customer was added to the store: registration or a first purchase without an account. |
Message body
The id field equals the portican-id-udalosti header and stays the same on every attempt. Amounts are in the smallest currency unit (cents). The format version is 1; fields may be added, existing ones do not change.
{
"id": "ud_3f9a2c7e1b5d4e8f9a0b1c2d3e4f5a6b",
"udalost": "objednavka.zaplatena",
"verzia": 1,
"obchod": "sk",
"vytvorena": "2026-10-20T10:15:00.000Z",
"data": {
"objednavka": {
"id": "1042",
"kod": "R3K9QZ",
"stav": "PaymentSettled",
"mena": "EUR",
"sumaSDph": 4890,
"sumaBezDph": 3976,
"polozky": [
{
"sku": "KV-ROZA-50",
"mnozstvo": 2,
"cenaZaKusSDph": 1995
}
],
"prijata": "2026-10-20T10:12:41.000Z"
}
}
}
Signature
- The
portican-podpisheader has the form t=time,v1=signature, as with Stripe. The signature is HMAC-SHA256 with the webhook secret over the time, a dot and the raw message body, written in hexadecimal. - The secret is shown once, when the webhook is added and when it is replaced; replacing it revokes the old secret immediately.
- Verify the raw body exactly as received, before parsing the JSON. Reject a message whose time is older than 300 seconds; every attempt carries a fresh time.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: request body exactly as received, before JSON.parse
function verifySignature(rawBody, header, secret, toleranceSeconds = 300) {
// header = request.headers['portican-podpis'], e.g. "t=1792500000,v1=5f2c..."
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const received = String(parts.v1 ?? '');
return expected.length === received.length && timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
Delivery and retries
- A message counts as delivered when a 2xx response arrives within 10 seconds. Respond right away and process it later. Redirects are not followed and count as a failure.
- After a failure the next attempt comes after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h. While the address keeps failing, it receives one attempt at a time and the other messages wait; the first successful response releases them too.
- After 8 failed attempts in a row (about two days) the webhook turns off and the store receives an e-mail. Once it is turned back on, undelivered messages can be sent again from the delivery log.
- The order of messages is not guaranteed and the same message can arrive twice. Use the vytvorena time in the body and the event id: drop a second message with the same id.
- Staff see the delivery log with the response code on the Integrations screen for 30 days.