Preskočiť na obsah
API dokumentácia

Recept: príjem webhookov

Cieľ: udržať CRM v súlade s Poštárnou: nové kontakty založiť, odhlásené označiť.

Potrebujete: kľúč s rolou account_admin (webhooky sú v module settings), e-mail člena firmy pre HTTP Basic, verejnú adresu https:// vášho príjemcu.

1. Registrácia

Požiadavka: POST /api/v3/web-hooks
curl -sS -X POST "https://www.postarna.cz/api/v3/web-hooks" \
  -u "$POSTARNA_USER:$POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "target_url": "https://crm.example.cz/postarna/webhook",
  "event": "new_contact",
  "basic_auth": {
    "login": "postarna",
    "password": "example-webhook-password"
  }
}
JSON
Odpoveď: 201 Created
{
  "status": "created",
  "meta": [],
  "id": 7
}
Požiadavka: POST /api/v3/web-hooks
curl -sS -X POST "https://www.postarna.cz/api/v3/web-hooks" \
  -u "$POSTARNA_USER:$POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "target_url": "https://crm.example.cz/postarna/webhook",
  "event": "unsubscribed_contact",
  "basic_auth": {
    "login": "postarna",
    "password": "example-webhook-password"
  }
}
JSON
Odpoveď: 201 Created
{
  "status": "created",
  "meta": [],
  "id": 8
}

Heslo basic_auth si vygenerujte náhodne (aspoň 32 znakov) a uložte do premennej prostredia príjemcu.

2. Čo prijde

Po založení a odhlásení kontaktu prídu dve volania:

Požiadavka: POST /api/v1/tenants/{tenant}/contacts
curl -sS -X POST "https://www.postarna.cz/api/v1/tenants/$TENANT/contacts" \
  -H "Authorization: Bearer $POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "email": "eva.horakova@example.cz",
  "shopId": "19",
  "purposeId": "10",
  "consentConfirmed": true,
  "firstName": "Eva",
  "lastName": "Horáková"
}
JSON
Odpoveď: 201 Created
{
  "id": "1207",
  "email": "eva.horakova@example.cz",
  "firstName": "Eva",
  "lastName": "Horáková",
  "status": "subscribed",
  "engagement": "N",
  "excluded": false,
  "shopId": "19",
  "suppressionReason": null,
  "blacklisted": false,
  "blacklistedAt": null,
  "state": null,
  "createdAt": "2026-10-01T08:00:00.000Z",
  "phone": "",
  "mobile": "",
  "titleBefore": "",
  "titleAfter": "",
  "salutation": "",
  "salutationFormal": "",
  "vocativeFirstName": "",
  "vocativeLastName": "",
  "gender": null,
  "language": "cs",
  "company": "",
  "companyId": "",
  "street": "",
  "city": "",
  "postalCode": "",
  "note": "",
  "affiliateId": "",
  "country": null,
  "birthday": null,
  "nameDay": null,
  "source": "manual",
  "consentBasis": "imported",
  "statusAt": "2026-10-01T08:00:00.000Z",
  "consentAt": "2026-10-01T08:00:00.000Z",
  "consentWeak": false,
  "softBounces": 0,
  "lastSentAt": null,
  "lastOpenAt": null,
  "lastClickAt": null,
  "suppressedAt": null,
  "engagementAt": null,
  "suppressionNote": null,
  "customFields": {},
  "updatedAt": "2026-10-01T08:00:00.000Z",
  "stats90d": {
    "received": 0,
    "opened": 0,
    "clicked": 0,
    "bestHour": null
  },
  "lists": [],
  "recentActivities": [
    {
      "id": "consent-8",
      "type": "status_change",
      "at": "2026-10-01T08:00:00.000Z",
      "title": "purpose_assigned"
    }
  ],
  "orders": [],
  "orderTotals": [],
  "editedFields": [],
  "sources": []
}
Požiadavka: POST /api/v1/tenants/{tenant}/contacts/1207/unsubscribe
curl -sS -X POST "https://www.postarna.cz/api/v1/tenants/$TENANT/contacts/1207/unsubscribe" \
  -H "Authorization: Bearer $POSTARNA_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "note": "Odhlášení z CRM"
}
JSON
Odpoveď: 200 OK
{
  "id": "1207",
  "email": "eva.horakova@example.cz",
  "firstName": "Eva",
  "lastName": "Horáková",
  "status": "unsubscribed",
  "engagement": "N",
  "excluded": false,
  "shopId": "19",
  "suppressionReason": "unsubscribed",
  "blacklisted": false,
  "blacklistedAt": null,
  "state": null,
  "createdAt": "2026-10-01T08:00:00.000Z",
  "phone": "",
  "mobile": "",
  "titleBefore": "",
  "titleAfter": "",
  "salutation": "",
  "salutationFormal": "",
  "vocativeFirstName": "",
  "vocativeLastName": "",
  "gender": null,
  "language": "cs",
  "company": "",
  "companyId": "",
  "street": "",
  "city": "",
  "postalCode": "",
  "note": "",
  "affiliateId": "",
  "country": null,
  "birthday": null,
  "nameDay": null,
  "source": "manual",
  "consentBasis": "imported",
  "statusAt": "2026-10-01T08:00:00.000Z",
  "consentAt": "2026-10-01T08:00:00.000Z",
  "consentWeak": false,
  "softBounces": 0,
  "lastSentAt": null,
  "lastOpenAt": null,
  "lastClickAt": null,
  "suppressedAt": "2026-10-01T08:00:00.000Z",
  "engagementAt": null,
  "suppressionNote": null,
  "customFields": {},
  "updatedAt": "2026-10-01T08:00:00.000Z",
  "stats90d": {
    "received": 0,
    "opened": 0,
    "clicked": 0,
    "bestHour": null
  },
  "lists": [],
  "recentActivities": [
    {
      "id": "consent-9",
      "type": "status_change",
      "at": "2026-10-01T08:00:00.000Z",
      "title": "suppressed"
    },
    {
      "id": "consent-8",
      "type": "status_change",
      "at": "2026-10-01T08:00:00.000Z",
      "title": "purpose_assigned"
    },
    {
      "id": "consent-10",
      "type": "unsubscribed",
      "at": "2026-10-01T08:00:00.000Z",
      "title": "admin"
    }
  ],
  "orders": [],
  "orderTotals": [],
  "editedFields": [],
  "sources": []
}
Webhook doručí (POST na vašu adresu): new_contact
[
  {
    "id": 1207,
    "guid": null,
    "emailaddress": "eva.horakova@example.cz",
    "domain": "example.cz",
    "name": "Eva",
    "surname": "Horáková",
    "titlesbefore": null,
    "titlesafter": null,
    "salution": null,
    "company": null,
    "street": null,
    "town": null,
    "postalcode": null,
    "country": null,
    "cellphone": null,
    "phone": null,
    "language": "cs_CZ",
    "birthday": null,
    "nameday": null,
    "gender": null,
    "notes": null,
    "realname": "Eva Horáková",
    "blacklisted": 0,
    "is_blacklisted": false,
    "hardbounced": 0,
    "softbounced": 0,
    "created": "2026-10-01 10:00:00",
    "updated": "2026-10-01 10:00:00",
    "created_at": "2026-10-01 10:00:00",
    "last_updated_at": "2026-10-01 10:00:00",
    "origin": "manual",
    "contactlists": [],
    "customfields": [],
    "event": "new_contact"
  }
]
Webhook doručí (POST na vašu adresu): unsubscribed_contact
[
  {
    "id": 1207,
    "guid": null,
    "emailaddress": "eva.horakova@example.cz",
    "domain": "example.cz",
    "name": "Eva",
    "surname": "Horáková",
    "titlesbefore": null,
    "titlesafter": null,
    "salution": null,
    "company": null,
    "street": null,
    "town": null,
    "postalcode": null,
    "country": null,
    "cellphone": null,
    "phone": null,
    "language": "cs_CZ",
    "birthday": null,
    "nameday": null,
    "gender": null,
    "notes": null,
    "realname": "Eva Horáková",
    "blacklisted": 0,
    "is_blacklisted": false,
    "hardbounced": 0,
    "softbounced": 0,
    "created": "2026-10-01 10:00:00",
    "updated": "2026-10-01 10:00:00",
    "created_at": "2026-10-01 10:00:00",
    "last_updated_at": "2026-10-01 10:00:00",
    "origin": "manual",
    "contactlists": [],
    "customfields": [],
    "event": "unsubscribed_contact",
    "reason": "admin",
    "unsubscribed_contactlist_ids": [],
    "campaign_type": null,
    "campaign_id": null,
    "email_statistic_id": null,
    "trigger_event": null
  }
]

3. Príjemca v Pythone (Flask)

app.py
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
USER = os.environ['POSTARNA_HOOK_USER']          # login z basic_auth
PASSWORD = os.environ['POSTARNA_HOOK_PASSWORD']  # heslo z basic_auth


def authorized():
    auth = request.authorization
    return bool(auth) and hmac.compare_digest(auth.username or '', USER) \
        and hmac.compare_digest(auth.password or '', PASSWORD)


@app.post('/postarna/webhook')
def postarna_webhook():
    if not authorized():
        abort(401)
    payload = request.get_json(silent=True)
    items = payload if isinstance(payload, list) else [payload]
    for item in items:
        event = item.get('event')
        if event == 'new_contact':
            crm_upsert_contact(email=item['emailaddress'], first_name=item.get('name'),
                               last_name=item.get('surname'))
        elif event == 'unsubscribed_contact':
            crm_mark_unsubscribed(email=item['emailaddress'], reason=item.get('reason'))
    return '', 204          # 2xx do 30 s, inak prijde znovu


def crm_upsert_contact(email, first_name, last_name):
    ...                     # založiť alebo upraviť podľa e-mailu (opakované doručenie neprekáža)


def crm_mark_unsubscribed(email, reason):
    ...                     # nastaviť príznak (opakované doručenie neprekáža)

4. Príjemca v Node.js (Express)

server.js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.use(express.json({ limit: '1mb' }));
const USER = process.env.POSTARNA_HOOK_USER;
const PASSWORD = process.env.POSTARNA_HOOK_PASSWORD;

function same(a, b) {
  const x = Buffer.from(a ?? '');
  const y = Buffer.from(b ?? '');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function authorized(req) {
  const [scheme, value] = (req.get('Authorization') ?? '').split(' ');
  if (scheme !== 'Basic' || !value) return false;
  const [user, ...rest] = Buffer.from(value, 'base64').toString('utf8').split(':');
  return same(user, USER) && same(rest.join(':'), PASSWORD);
}

app.post('/postarna/webhook', async (req, res) => {
  if (!authorized(req)) return res.sendStatus(401);
  const items = Array.isArray(req.body) ? req.body : [req.body];
  for (const item of items) {
    if (item.event === 'new_contact') await crmUpsertContact(item.emailaddress, item.name, item.surname);
    if (item.event === 'unsubscribed_contact') await crmMarkUnsubscribed(item.emailaddress, item.reason);
  }
  res.sendStatus(204); // 2xx do 30 s, inak prijde znovu
});

app.listen(8080);

5. Pravidlá dobrého príjemcu

  • Odpovedajte rýchlo: dlhšie spracovanie dajte do radu a hneď vráťte 2xx. Po 30 sekundách sa volanie počíta ako chyba.
  • Zvládnete opakovanie: rovnaká položka môže prísť viackrát. Spracovanie robte idempotentné (upsert podľa e-mailu, nastavenie príznaku), pri e-mailových udalostiach si pamätajte id.
  • Neodpovedajte chybou na neznámu udalosť: vráťte 2xx, inak sa webhook po 10 chybách vypne.
  • Sledujte stav: GET /api/v3/web-hooks ukazuje active, last_response_code, last_call_time a errors_in_row.
  • Odhlásenie v CRM pošlite späť do Poštárne cez POST /api/v1/tenants/{tenant}/contacts/{id}/unsubscribe, aby obe strany držali rovnaký stav.