Skip to content
Online store and company website. No cut of your revenue. Start for free.

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-key header and the market token in the vendure-token header; 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 activeChannel query; 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.

ScopeWhat it allows
Read orders
objednavky-citanie
Orders with items and totals, including the customer details in them (name, delivery address).
order, orders
Fulfilment
expedicia
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 catalogue
katalog-citanie
Products, variants with stock, collections and facets.
product, products, productVariant, productVariants, collection, collections, facet, facets
Edit catalogue
katalog-zapis
Change products and variants, including price and stock, without deleting anything.
product, products, productVariant, productVariants, collection, collections, facet, facets, updateProduct, updateProductVariants
Read customers
zakaznici-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 a skuska event.
  • 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.

EventWhen it is sent
objednavka.prijataThe customer completed an order (also when paying by bank transfer or cash on delivery).
objednavka.zaplatenaThe order is fully paid: card payment, bank transfer marked as received, or cash on delivery after delivery.
objednavka.odoslanaA shipment left, for the whole order or a part of it (field stav).
objednavka.dorucenaA shipment was delivered, for the whole order or a part of it (field stav).
objednavka.zrusenaA placed order was cancelled.
zasoba.zmenenaThe stock or the reserved quantity of a variant changed: restocking, sale, reservation in an order, cancellation.
zakaznik.novyA 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-podpis header 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.