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
| Co | Cesta | Kdy |
|---|---|---|
| Objednávky | POST /orders | po vytvoření objednávky a po každé změně stavu |
| Zákazníci | vzniknou z objednávky | kupující bez souhlasu je zákazník bez marketingového souhlasu (customer), e-maily nedostává |
| Odběratelé | POST /contacts se souhlasem | jen když člověk souhlas opravdu dal (formulář, pokladna) |
| Události | POST /contact-events | košík, návštěva, vlastní událost (například registrace do věrnostního programu) |
| Z prohlížeče | jeden řádek postarna.js | zobrazení 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é.
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(apaidAt, když je zaplacená). statusje název stavu ve vašem e-shopu („processing“, „Zaplaceno“ ...). Na kanonický stav ho převede mapa stavů; název bez pravidla jeothera nikdy se nepočítá jako nákup.currencysmí 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:00nebo...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 sRetry-After).
Mapování polí
| Poštárna | WooCommerce (REST, webhook) | Shopify (webhook orders/*) | Poznámka |
|---|---|---|---|
id | id | id | stabilní a jedinečné v e-shopu |
eshopCode | number | name (#1001) | číslo, které vidí zákazník |
createdAt | date_created_gmt + Z | created_at | se zónou |
paidAt | date_paid_gmt + Z | podle financial_status | |
status | status | financial_status, cancelled_at | název stavu, mapa stavů ho převede |
email | billing.email | email | |
currency | currency | currency | měna e-shopu |
totalWithVat | total | total_price | |
totalWithoutVat | total minus total_tax | total_price minus total_tax | |
items[].id | line_items[].sku (jinak product_id) | line_items[].sku (jinak variant_id) | stejný kód jako v produktovém feedu |
items[].name, quantity | line_items[].name, quantity | line_items[].title, quantity | |
items[].price.withoutVat | line_items[].price | jednotková cena | |
items[].price.withVat | line_items[].price (podle nastavení daní obchodu) | jednotková cena | |
promoCodesUsed | coupon_lines[].code | discount_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 /contactss účelem zpracování a potvrzením, že souhlas máte doložený. Bez souhlasu kontakt přes API nezakládejte.
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řesPATCH /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.
type | Tělo | K čemu |
|---|---|---|
cart | shopId, items (code, name, quantity, price, url), currency, total | celý obsah košíku, prázdný = vyprázdněný; Opuštěný košík |
visit | shopId, url, visitType (product, category, cart, order_done ...) | návštěva stránky |
custom | name (malá písmena, číslice, pomlčky), data (objekt) | vlastní událost pro spouštěč Vlastní událost |
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):
<?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):
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 (
messagenese 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-statesvseenukáž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.