Přeskočit na obsah
API dokumentace

Objednávky a události přes API a webhook

Tahle stránka je pro e-shop na jakékoli platformě, který Poštárna nenapojí sama (feed, import, jeden řádek kódu). Objednávky, zákazníky a události pošlete přes REST API v1; Poštárna z nich spouští automatizace po nákupu, počítá tržby z e-mailů a plní segmenty.

Potřebujete: API klíč s rolí marketing_manager (oblasti contacts a campaigns), ID firmy ({tenant}) a ID e-shopu (shopId), postup v Začínáme. Ke stažení: kolekce pro Postman se všemi požadavky z této stránky (vyplníte proměnné apiKey, tenant, shopId).

Tři cesty, jedna pravda

CoCestaKdy
ObjednávkyPOST /orderspo vytvoření objednávky a po každé změně stavu
Zákaznícivzniknou z objednávkykupující bez souhlasu je zákazník bez marketingového souhlasu (customer), e-maily nedostává
OdběrateléPOST /contacts se souhlasemjen když člověk souhlas opravdu dal (formulář, pokladna)
UdálostiPOST /contact-eventskošík, návštěva, vlastní událost (například registrace do věrnostního programu)
Z prohlížečejeden řádek postarna.jszobrazení produktu, košík, pokladna, objednávka; popis v aplikaci a v integracích

Objednávky

Objednávka se ukládá podle dvojice (shopId, id), kde id je číslo objednávky ve vašem e-shopu. To je klíč idempotence: stejná objednávka poslaná znovu vrátí 200 a "change": "unchanged" a nic nezmění; nová vrátí 201 created, změněná 200 updated. Opakovat požadavek po výpadku sítě je proto vždy bezpečné.

Terminál
curl -sS -X POST "https://www.postarna.cz/api/v1/tenants/$TENANT/orders" \
  -H "Authorization: Bearer $POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "id": "2026001234",
  "shopId": "1",
  "createdAt": "2026-10-01T09:30:00+02:00",
  "email": "jana.novakova@example.cz",
  "status": "Vyřizuje se",
  "currency": "CZK",
  "totalWithVat": "1290.00",
  "totalWithoutVat": "1066.12",
  "items": [
    {"id": "KAVA-1KG", "name": "Káva Kolumbie 1 kg", "quantity": 1,
     "price": {"withVat": "1290.00", "withoutVat": "1066.12", "currency": "CZK"},
     "attributes": {"Category": "Káva", "Brand": "Example"}}
  ]
}
JSON
  • Každý zápis nahrazuje celou objednávku. Při změně stavu pošlete celou objednávku znovu s novým status (a paidAt, když je zaplacená).
  • status je název stavu ve vašem e-shopu („processing“, „Zaplaceno“ ...). Na kanonický stav ho převede mapa stavů; název bez pravidla je other a nikdy se nepočítá jako nákup.
  • currency smí být jen měna e-shopu (e-shop v CZK nepřijme EUR). Částky jsou desetinné řetězce v měně objednávky; Poštárna je nikdy nepřevádí ani nesčítá přes měny.
  • Časy ISO 8601 se zónou (2026-10-01T09:30:00+02:00 nebo ...Z). Čas bez zóny je chyba 400.
  • Smazaná objednávka: "deleted": true (běh po nákupu se ukončí).
  • Kupující se spáruje s kontaktem podle e-mailu; bez kontaktu vznikne zákazník bez marketingového souhlasu.
  • Limity: 2 500 požadavků nárazově, doplňuje se 4 za sekundu, 100 000 za den na firmu (hlavičky X-RateLimit-*, po vyčerpání 429 s Retry-After).

Mapování polí

PoštárnaWooCommerce (REST, webhook)Shopify (webhook orders/*)Poznámka
idididstabilní a jedinečné v e-shopu
eshopCodenumbername (#1001)číslo, které vidí zákazník
createdAtdate_created_gmt + Zcreated_atse zónou
paidAtdate_paid_gmt + Zpodle financial_status
statusstatusfinancial_status, cancelled_atnázev stavu, mapa stavů ho převede
emailbilling.emailemail
currencycurrencycurrencyměna e-shopu
totalWithVattotaltotal_price
totalWithoutVattotal minus total_taxtotal_price minus total_tax
items[].idline_items[].sku (jinak product_id)line_items[].sku (jinak variant_id)stejný kód jako v produktovém feedu
items[].name, quantityline_items[].name, quantityline_items[].title, quantity
items[].price.withoutVatline_items[].pricejednotková cena
items[].price.withVatline_items[].price (podle nastavení daní obchodu)jednotková cena
promoCodesUsedcoupon_lines[].codediscount_codes[].code

Názvy polí platforem uvádíme podle jejich veřejné dokumentace; na živém obchodě je ověřte, než napojení zapnete.

Doporučená mapa stavů WooCommerce (PUT /order-states): pending a on-hold = unpaid, processing = paid, completed = delivered, cancelled a failed = cancelled, refunded = returned.

Zákazníci a odběratelé

  • Zákazník nepotřebuje vlastní volání: vznikne z první objednávky s jeho e-mailem jako kontakt ve stavu customer (bez marketingového souhlasu). Automatizace po nákupu a zákaznická výjimka (soft opt-in) s ním pracují podle nastavení e-shopu.
  • Odběratel se souhlasem: POST /contacts s účelem zpracování a potvrzením, že souhlas máte doložený. Bez souhlasu kontakt přes API nezakládejte.
Terminál
curl -sS -X POST "https://www.postarna.cz/api/v1/tenants/$TENANT/contacts" \
  -H "Authorization: Bearer $POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "petr.svoboda@example.cz", "shopId": "1", "firstName": "Petr", "purposeId": "1", "consentConfirmed": true}'
  • Existující adresa vrátí 409 conflict; úpravy přes PATCH /contacts/{id}. Odhlášený nebo potlačený kontakt se přes API nikdy znovu nepřihlásí. Podrobnosti v Kontakty a souhlasy.

Události

POST /contact-events zapíše událost existujícího kontaktu (podle email nebo contactId); kontakt nikdy nezaloží. Spouštěče automatizací na ni reagují hned.

typeTěloK čemu
cartshopId, items (code, name, quantity, price, url), currency, totalcelý obsah košíku, prázdný = vyprázdněný; Opuštěný košík
visitshopId, url, visitType (product, category, cart, order_done ...)návštěva stránky
customname (malá písmena, číslice, pomlčky), data (objekt)vlastní událost pro spouštěč Vlastní událost
Terminál
curl -sS -X POST "https://www.postarna.cz/api/v1/tenants/$TENANT/contact-events" \
  -H "Authorization: Bearer $POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "cart", "email": "jana.novakova@example.cz", "shopId": "1", "currency": "CZK", "total": "1290.00", "items": [{"code": "KAVA-1KG", "name": "Káva Kolumbie 1 kg", "quantity": 1, "price": "1290.00"}]}'
  • Odpověď 201 {"id": "..."}, neznámý kontakt 404, nejvýš 60 událostí jednoho kontaktu za 10 minut (pak 429).
  • Události nejsou idempotentní: opakovaný požadavek zapíše událost znovu. Košík posílejte vždy celý (opakování pak nic nepokazí), a co se má započítat jen jednou, pošlete jako objednávku.

Webhooky

Z Poštárny k vám

Účtové webhooky pošlou na vaši adresu nový kontakt, změnu, odhlášení, odeslání, doručení, otevření a klik. Registrace, tvar dat a ukázkový příjemce: Webhooky a Recept: příjem webhooků.

Z e-shopu do Poštárny

WooCommerce i Shopify umí při nové a změněné objednávce zavolat webhook, ale neumí poslat hlavičku Authorization s API klíčem. Proto mezi ně a Poštárnu patří krátký převodník na vašem serveru (nebo v Make, n8n či Zapier): ověří podpis e-shopu, převede pole podle tabulky výše a zavolá POST /orders. Hotový plugin pro WooCommerce a modul pro Make a n8n zatím nemáme.

PHP (WooCommerce, webhook Objednávka vytvořena a Objednávka aktualizována):

postarna-woo.php
<?php
// Secret of the WooCommerce webhook and the Poštárna API key come from the environment, never from the code.
$secret = getenv('WOO_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
if (!hash_equals(base64_encode(hash_hmac('sha256', $raw, $secret, true)), $sig)) {
    http_response_code(401);
    exit;
}
$o = json_decode($raw, true);
if (!isset($o['id'])) {            // the ping WooCommerce sends when the webhook is saved
    http_response_code(204);
    exit;
}
$items = [];
foreach ($o['line_items'] as $li) {
    $items[] = [
        'id' => $li['sku'] !== '' ? $li['sku'] : (string) $li['product_id'],
        'name' => $li['name'],
        'quantity' => $li['quantity'],
        'price' => ['withoutVat' => number_format((float) $li['price'], 2, '.', ''), 'currency' => $o['currency']],
    ];
}
$order = [
    'id' => (string) $o['id'],
    'shopId' => getenv('POSTARNA_SHOP_ID'),
    'eshopCode' => (string) $o['number'],
    'createdAt' => $o['date_created_gmt'] . 'Z',
    'paidAt' => $o['date_paid_gmt'] ? $o['date_paid_gmt'] . 'Z' : null,
    'status' => $o['status'],
    'email' => $o['billing']['email'] ?? null,
    'currency' => $o['currency'],
    'totalWithVat' => $o['total'],
    'totalWithoutVat' => number_format((float) $o['total'] - (float) $o['total_tax'], 2, '.', ''),
    'promoCodesUsed' => array_map(fn ($c) => $c['code'], $o['coupon_lines'] ?? []),
    'items' => $items,
];
$ch = curl_init('https://www.postarna.cz/api/v1/tenants/' . getenv('POSTARNA_TENANT') . '/orders');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 20,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('POSTARNA_API_KEY'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode($order),
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
// 5xx or 429: answer 500 so WooCommerce delivers the webhook again (the same order is idempotent).
http_response_code($status >= 500 || $status === 429 || $status === 0 ? 500 : 204);

JavaScript (Node.js 20+, Shopify webhook orders/create a orders/updated):

postarna-shopify.mjs
import crypto from 'node:crypto';
import http from 'node:http';

const { SHOPIFY_WEBHOOK_SECRET, POSTARNA_API_KEY, POSTARNA_TENANT, POSTARNA_SHOP_ID } = process.env;

function signed(raw, header) {
  const digest = crypto.createHmac('sha256', SHOPIFY_WEBHOOK_SECRET).update(raw).digest('base64');
  const a = Buffer.from(digest);
  const b = Buffer.from(header ?? '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', async () => {
    const raw = Buffer.concat(chunks);
    if (!signed(raw, req.headers['x-shopify-hmac-sha256'])) return res.writeHead(401).end();
    const o = JSON.parse(raw.toString('utf8'));
    const order = {
      id: String(o.id),
      shopId: POSTARNA_SHOP_ID,
      eshopCode: o.name,
      createdAt: o.created_at,
      status: o.cancelled_at ? 'cancelled' : o.financial_status,
      email: o.email || null,
      currency: o.currency,
      totalWithVat: o.total_price,
      totalWithoutVat: (Number(o.total_price) - Number(o.total_tax || 0)).toFixed(2),
      promoCodesUsed: (o.discount_codes || []).map((d) => d.code),
      items: o.line_items.map((li) => ({
        id: li.sku || String(li.variant_id), name: li.title, quantity: li.quantity,
        price: { withVat: li.price, currency: o.currency },
      })),
    };
    const r = await fetch(`https://www.postarna.cz/api/v1/tenants/${POSTARNA_TENANT}/orders`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${POSTARNA_API_KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(order),
    }).catch(() => ({ status: 0 }));
    // 5xx, 429 or no answer: 500 makes Shopify retry; the same order is idempotent
    res.writeHead(r.status >= 500 || r.status === 429 || r.status === 0 ? 500 : 204).end();
  });
}).listen(8080);
  • Převodník odpovídá e-shopu rychle (do pár sekund), jinak e-shop webhook zopakuje nebo vypne.
  • Při chybě Poštárny (5xx, 429) vraťte e-shopu chybu, ať webhook zopakuje. Objednávka je idempotentní, opakování nic nezdvojí.
  • Chyba 400 znamená vadná data (message nese název pole): opravte převodník, opakování nepomůže.

Kontrola

  • GET /orders?contactId={id} ukáže objednávky kontaktu, GET /orders/{id} detail se stavem a konverzí.
  • GET /order-states v seen ukáže názvy stavů, které přišly, a co z nich mapa udělala. Nové názvy namapujte dřív, než zapnete automatizace po nákupu.
  • Automatizace po nákupu: Recept: série po nákupu; opuštěný košík: Recept: opuštěný košík.