Objednávky a udalosti cez API a webhook
Táto stránka je pre e-shop na akejkoľvek platforme, ktorý Poštárna nenapojí sama (feed, import, jeden riadok kódu). Objednávky, zákazníkov a udalosti pošlete cez REST API v1; Poštárna z nich spúšťa automatizácie po nákupe, počíta tržby z e-mailov a plní segmenty.
Potrebujete: API kľúč s rolou marketing_manager (oblasti contacts a campaigns), ID firmy ({tenant}) a ID e-shopu (shopId), postup v Začíname. Na stiahnutie: kolekcia pre Postman so všetkými požiadavkami z tejto stránky (vyplníte premenné apiKey, tenant, shopId).
Tri cesty, jedna pravda
| Čo | Cesta | Kedy |
|---|---|---|
| Objednávky | POST /orders | po vytvorení objednávky a po každej zmene stavu |
| Zákazníci | vzniknú z objednávky | kupujúci bez súhlasu je zákazník bez marketingového súhlasu (customer), e-maily nedostáva |
| Odberatelia | POST /contacts so súhlasom | len keď človek súhlas naozaj dal (formulár, pokladňa) |
| Udalosti | POST /contact-events | košík, návšteva, vlastná udalosť (napríklad registrácia do vernostného programu) |
| Z prehliadača | jeden riadok postarna.js | zobrazenie produktu, košík, pokladňa, objednávka; popis v aplikácii a v integráciách |
Objednávky
Objednávka sa ukladá podľa dvojice (shopId, id), kde id je číslo objednávky vo vašom e-shope. To je kľúč idempotencie: tá istá objednávka poslaná znova vráti 200 a "change": "unchanged" a nič nezmení; nová vráti 201 created, zmenená 200 updated. Opakovať požiadavku po výpadku siete je preto vždy bezpečné.
curl -sS -X POST "https://www.postarna.sk/api/v1/tenants/$TENANT/orders" \
-H "Authorization: Bearer $POSTARNA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{
"id": "2026001234",
"shopId": "2",
"createdAt": "2026-10-01T09:30:00+02:00",
"email": "jana.novakova@example.sk",
"status": "Vybavuje sa",
"currency": "EUR",
"totalWithVat": "52.90",
"totalWithoutVat": "43.00",
"items": [
{"id": "KAVA-1KG", "name": "Káva Kolumbia 1 kg", "quantity": 1,
"price": {"withVat": "52.90", "withoutVat": "43.00", "currency": "EUR"},
"attributes": {"Category": "Káva", "Brand": "Example"}}
]
}
JSON- Každý zápis nahrádza celú objednávku. Pri zmene stavu pošlite celú objednávku znova s novým
status(apaidAt, keď je zaplatená). statusje názov stavu vo vašom e-shope („processing“, „Zaplatená“ ...). Na kanonický stav ho prevedie mapa stavov; názov bez pravidla jeothera nikdy sa nepočíta ako nákup.currencysmie byť len mena e-shopu (e-shop v EUR neprijme CZK). Sumy sú desatinné reťazce v mene objednávky; Poštárna ich nikdy neprevádza ani nesčítava naprieč menami.- Časy ISO 8601 so zónou (
2026-10-01T09:30:00+02:00alebo...Z). Čas bez zóny je chyba 400. - Zmazaná objednávka:
"deleted": true(beh po nákupe sa ukončí). - Kupujúci sa spáruje s kontaktom podľa e-mailu; bez kontaktu vznikne zákazník bez marketingového súhlasu.
- Limity: 2 500 požiadaviek nárazovo, dopĺňa sa 4 za sekundu, 100 000 za deň na firmu (hlavičky
X-RateLimit-*, po vyčerpaní 429 sRetry-After).
Mapovanie polí
| Poštárna | WooCommerce (REST, webhook) | Shopify (webhook orders/*) | Poznámka |
|---|---|---|---|
id | id | id | stabilné a jedinečné v e-shope |
eshopCode | number | name (#1001) | číslo, ktoré vidí zákazník |
createdAt | date_created_gmt + Z | created_at | so zónou |
paidAt | date_paid_gmt + Z | podľa financial_status | |
status | status | financial_status, cancelled_at | názov stavu, mapa stavov ho prevedie |
email | billing.email | email | |
currency | currency | currency | mena e-shopu |
totalWithVat | total | total_price | |
totalWithoutVat | total mínus total_tax | total_price mínus total_tax | |
items[].id | line_items[].sku (inak product_id) | line_items[].sku (inak variant_id) | ten istý kód ako v produktovom feede |
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 (podľa nastavenia daní obchodu) | jednotková cena | |
promoCodesUsed | coupon_lines[].code | discount_codes[].code |
Názvy polí platforiem uvádzame podľa ich verejnej dokumentácie; na živom obchode ich overte skôr, ako napojenie zapnete.
Odporúčaná mapa stavov WooCommerce (PUT /order-states): pending a on-hold = unpaid, processing = paid, completed = delivered, cancelled a failed = cancelled, refunded = returned.
Zákazníci a odberatelia
- Zákazník nepotrebuje vlastné volanie: vznikne z prvej objednávky s jeho e-mailom ako kontakt v stave
customer(bez marketingového súhlasu). Automatizácie po nákupe a zákaznícka výnimka (soft opt-in) s ním pracujú podľa nastavenia e-shopu. - Odberateľ so súhlasom:
POST /contactss účelom spracovania a potvrdením, že súhlas máte doložený. Bez súhlasu kontakt cez API nezakladajte.
curl -sS -X POST "https://www.postarna.sk/api/v1/tenants/$TENANT/contacts" \
-H "Authorization: Bearer $POSTARNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "peter.horvath@example.sk", "shopId": "2", "firstName": "Peter", "purposeId": "1", "consentConfirmed": true}'- Existujúca adresa vráti 409
conflict; úpravy cezPATCH /contacts/{id}. Odhlásený alebo potlačený kontakt sa cez API nikdy znova neprihlási. Podrobnosti v Kontakty a súhlasy.
Udalosti
POST /contact-events zapíše udalosť existujúceho kontaktu (podľa email alebo contactId); kontakt nikdy nezaloží. Spúšťače automatizácií na ňu reagujú hneď.
type | Telo | Na čo |
|---|---|---|
cart | shopId, items (code, name, quantity, price, url), currency, total | celý obsah košíka, prázdny = vyprázdnený; Opustený košík |
visit | shopId, url, visitType (product, category, cart, order_done ...) | návšteva stránky |
custom | name (malé písmená, číslice, pomlčky), data (objekt) | vlastná udalosť pre spúšťač Vlastná udalosť |
curl -sS -X POST "https://www.postarna.sk/api/v1/tenants/$TENANT/contact-events" \
-H "Authorization: Bearer $POSTARNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "cart", "email": "jana.novakova@example.sk", "shopId": "2", "currency": "EUR", "total": "52.90", "items": [{"code": "KAVA-1KG", "name": "Káva Kolumbia 1 kg", "quantity": 1, "price": "52.90"}]}'- Odpoveď 201
{"id": "..."}, neznámy kontakt 404, najviac 60 udalostí jedného kontaktu za 10 minút (potom 429). - Udalosti nie sú idempotentné: opakovaná požiadavka zapíše udalosť znova. Košík posielajte vždy celý (opakovanie potom nič nepokazí), a čo sa má započítať len raz, pošlite ako objednávku.
Webhooky
Z Poštárny k vám
Účtové webhooky pošlú na vašu adresu nový kontakt, zmenu, odhlásenie, odoslanie, doručenie, otvorenie a klik. Registrácia, tvar dát a ukážkový príjemca: Webhooky a Recept: príjem webhookov.
Z e-shopu do Poštárny
WooCommerce aj Shopify vedia pri novej a zmenenej objednávke zavolať webhook, ale nevedia poslať hlavičku Authorization s API kľúčom. Preto medzi ne a Poštárnu patrí krátky prevodník na vašom serveri (alebo v Make, n8n či Zapier): overí podpis e-shopu, prevedie polia podľa tabuľky vyššie a zavolá POST /orders. Hotový plugin pre WooCommerce a modul pre Make a n8n zatiaľ nemáme.
PHP (WooCommerce, webhook Objednávka vytvorená a Objednávka aktualizovaná):
<?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.sk/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.sk/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);- Prevodník odpovedá e-shopu rýchlo (do pár sekúnd), inak e-shop webhook zopakuje alebo vypne.
- Pri chybe Poštárny (5xx, 429) vráťte e-shopu chybu, nech webhook zopakuje. Objednávka je idempotentná, opakovanie nič nezdvojí.
- Chyba 400 znamená chybné dáta (
messagenesie názov poľa): opravte prevodník, opakovanie nepomôže.
Kontrola
GET /orders?contactId={id}ukáže objednávky kontaktu,GET /orders/{id}detail so stavom a konverziou.GET /order-statesvseenukáže názvy stavov, ktoré prišli, a čo z nich mapa urobila. Nové názvy namapujte skôr, ako zapnete automatizácie po nákupe.- Automatizácie po nákupe: Recept: séria po nákupe; opustený košík: Recept: opustený košík.