Webhookok
Hogyan működnek a webhookok
Szekció neve “Hogyan működnek a webhookok”A webhook a te HTTPS (vagy HTTP) végpontod, amelyet az Echo POST-kéréssel hív meg, amikor a feliratkozott események bekövetkeznek. Webhookot a felületen az Integrációk, Webhookok úton, vagy az API-n át (lent) hozhatsz létre. Minden webhooknak saját aláírókulcsa van.
- Az Echo csak akkor gyűjti az eseményeket, ha legalább egy bekapcsolt webhook figyeli őket. Azok az események, amelyek a webhook kikapcsolt állapotában vagy a létrehozása előtt történtek, soha nem mennek ki.
- Az Echo néhány másodpercenként nézi az új eseményeket. A kézbesítések sorrendje felcserélődhet (egy újrapróbált egy újabb után érkezik), ezért az eseményeket az
occurred_atszerint rendezd, ne az érkezés szerint. - A tartalom a kézbesítés előkészítésekor, az akkori adatokból készül. Az időközben törölt feliratkozó kimarad az eseményből.
- Egy munkaterületnek legfeljebb 20 webhookja lehet.
Események
Szekció neve “Események”| Esemény | Mikor megy | Kulcsok az eseményben |
|---|---|---|
subscriber.created |
Bármilyen módon új feliratkozó kerül be (felület, import, API, feliratkozás). | subscriber |
subscriber.updated |
Megváltozik a feliratkozó e-mail-címe vagy mezőértéke. | subscriber |
subscriber.active |
Az állapot active lesz (például dupla megerősítés után). |
subscriber |
subscriber.unsubscribed |
Az állapot unsubscribed lesz. |
subscriber |
subscriber.bounced |
Az állapot bounced lesz. |
subscriber |
subscriber.spam_reported |
Az állapot junk lesz (spamjelentés). |
subscriber |
subscriber.deleted |
A feliratkozót törlik vagy elfelejtik. Csak kötegelve. | subscriber (rövid forma) |
subscriber.added_to_group |
A feliratkozó bekerül egy csoportba. | subscriber, group |
subscriber.removed_from_group |
A feliratkozót kiveszik egy csoportból (a csoport vagy a feliratkozó törlésekor nem). | subscriber, group |
subscriber.automation_triggered |
A feliratkozó belép egy munkafolyamatba. | subscriber, automation |
subscriber.automation_completed |
A feliratkozó végigér egy munkafolyamaton. | subscriber, automation |
campaign.sent |
Egy kampány kiküldése befejeződik (a munkafolyamatok levelei nem). | campaign |
campaign.open |
Egy kampánylevél minden megnyitása. Csak kötegelve. | subscriber, campaign, unique |
campaign.click |
Minden kattintás egy kampánylevélben. Csak kötegelve. | subscriber, campaign, unique, url |
A „csak kötegelve” küldhető eseményeket csak akkor választhatod, ha a webhooknál be van kapcsolva a batchable.
Az Echo által küldött kérés
Szekció neve “Az Echo által küldött kérés”POST /hooks/echo HTTP/1.1Host: crm.example.comContent-Type: application/jsonUser-Agent: upmail-webhookX-Upmail-Signature: sha256=<64 hex characters>X-Upmail-Delivery: 3b0f6a52-8c1e-4d7a-9f20-6e4b1c2d3a45X-Upmail-Timestamp: 1792659131
{"delivery_id":"3b0f6a52-8c1e-4d7a-9f20-6e4b1c2d3a45","webhook_id":"8c17fb2c-011a-4058-a958-b56459fb2c4c","sent_at":"2026-10-22T08:52:11Z","events":[...]}| Fejléc | Tartalom |
|---|---|
X-Upmail-Signature |
sha256= és a nyers kéréstörzs hexadecimális HMAC-SHA256 értéke, a webhook aláírókulcsával. |
X-Upmail-Delivery |
A kézbesítés azonosítója, ugyanaz, mint a törzsben a delivery_id. |
X-Upmail-Timestamp |
A sent_at Unix-másodpercben. |
| Törzskulcs | Tartalom |
|---|---|
delivery_id |
A kézbesítés azonosítója. Az automatikus újrapróbálásoknál ugyanaz marad. A felületről indított kézi újraküldés új kézbesítés, saját delivery_id értékkel. |
webhook_id |
A webhook azonosítója. |
sent_at |
Mikor ment ki ez a kísérlet (UTC, egész másodperc). Minden kísérletnek saját értéke van. |
test |
Tesztkézbesítésnél (Teszt küldése a felületen) true, egyébként hiányzik. |
resent_from |
Kézi újraküldésnél annak a kézbesítésnek a delivery_id értéke, amelyet újraküld. Egyébként hiányzik. |
events |
Az események: egy, kötegelt webhooknál legfeljebb 100. |
Az események tartalma
Szekció neve “Az események tartalma”Minden eseményben van type és occurred_at (UTC), plusz a típusához tartozó kulcsok. A subscriber az API teljes feliratkozó objektuma. Az üzenet minden időpontja, a feliratkozóé is, UTC-ben érkezik (RFC 3339, Z végződéssel), ugyanúgy, mint az API válaszaiban.
subscriber.created
{ "delivery_id": "3b0f6a52-8c1e-4d7a-9f20-6e4b1c2d3a45", "webhook_id": "8c17fb2c-011a-4058-a958-b56459fb2c4c", "sent_at": "2026-10-22T08:52:11Z", "events": [ { "type": "subscriber.created", "occurred_at": "2026-10-22T08:52:08.527488Z", "subscriber": { "id": "d5f0b4b1-b59d-40eb-b2e0-74857893a055", "status": "unconfirmed", "source": "api", "fields": {"city": "Szeged", "last_name": "Farkas", "name": "Lilla", "rendelesek_szama": 3}, "groups": [{"id": "257e83a2-d55e-4a8c-b9a4-b3e939c151cc", "name": "Webshop vásárlók"}], "signup_ip": "203.0.113.25", "optin_ip": null, "subscribed_at": "2026-10-22T08:52:08.527488Z", "opted_in_at": null, "unsubscribed_at": null, "timezone": null, "location_country": null, "location_city": null, "sent": 0, "opens": 0, "clicks": 0, "open_rate": 0, "click_rate": 0, "created_at": "2026-10-22T08:52:08.527488Z", "updated_at": "2026-10-22T08:52:08.527488Z" } } ]}A subscriber.added_to_group és a subscriber.removed_from_group a csoportot is viszi:
{"type":"subscriber.added_to_group","occurred_at":"2026-10-22T05:53:30.112623Z","subscriber":{"id":"d5f0b4b1-b59d-40eb-b2e0-74857893a055","email":"[email protected]","...":"..."},"group":{"id":"257e83a2-d55e-4a8c-b9a4-b3e939c151cc","name":"Webshop vásárlók"}}A subscriber.automation_triggered és a subscriber.automation_completed a munkafolyamatot is viszi:
{"type":"subscriber.automation_triggered","occurred_at":"2026-10-21T16:51:00.565674Z","subscriber":{"...":"..."},"automation":{"id":"477323d4-1ba3-4658-b70d-a7d94ceed56b","name":"Welcome email (EN)"}}A subscriber.deleted rövid feliratkozót visz: az id, email és status értéket a törlés előtti állapot szerint. GDPR szerinti elfelejtés után csak az azonosítót és egy jelzőt:
{"type":"subscriber.deleted","occurred_at":"2026-10-22T05:59:03Z","subscriber":{"id":"8273ff7d-cb69-4a72-83e6-dbebbec33115","email":"[email protected]","status":"active"}}{"type":"subscriber.deleted","occurred_at":"2026-10-22T05:59:04Z","subscriber":{"id":"98cc16c4-cb25-4ec6-8dbe-9704c4e198c4","forgotten":true}}Ha "forgotten": true érkezik, töröld a személy adatait a saját rendszeredben is.
campaign.sent
{"type":"campaign.sent","occurred_at":"2026-10-20T08:02:25.333333Z","campaign":{"id":"30bc6133-16ad-4074-b311-204127a80b62","name":"Winter-ready garden (EN)","type":"regular"}}A campaign.open és a campaign.click minden megnyitásról és kattintásról megy. A unique értéke true az adott feliratkozó első megnyitásánál (vagy első kattintásánál) abban a levélben. Az url a link úgy, ahogy a kampányban tárolva van, kitöltetlen címkékkel:
{"type":"campaign.click","occurred_at":"2026-10-20T08:47:18.826706Z","subscriber":{"...":"..."},"campaign":{"id":"30bc6133-16ad-4074-b311-204127a80b62","name":"Winter-ready garden (EN)","type":"regular"},"unique":true,"url":"https://kertlak.example.com/teli-felkeszites?utm_source=kertlak"}A tesztkézbesítések "test": true jelzést és egy eseményt visznek, a webhook első eseménytípusával, mintaadatokkal ([email protected], „Sample group”, „Sample workflow”, „Sample campaign”).
A tartalmat rugalmasan olvasd: a már nem létező hivatkozott objektum kimarad, és új kulcsok is megjelenhetnek.
Az aláírás ellenőrzése
Szekció neve “Az aláírás ellenőrzése”Számold ki a HMAC-SHA256 értéket a nyers kéréstörzsön (a pontos bájtokon, JSON-feldolgozás előtt), kulcsként az aláírókulccsal, és a hexadecimális alakját hasonlítsd össze az X-Upmail-Signature fejlécben a sha256= utáni résszel. A kulcs a felületen mutatott szöveg pontosan (UTF-8 bájtjai); ne dekódold hexából vagy base64-ből. Állandó idejű összehasonlítást használj.
A visszajátszott kérések ellen azt is ellenőrizd, hogy az X-Upmail-Timestamp közel van-e a saját órádhoz. Minden kísérletet friss időbélyeggel ír alá újra az Echo, így néhány perces ablak elég. A példák 5 percet használnak; válaszd azt, ami nálad beválik.
JavaScript (Node.js, keretrendszer nélkül)
import crypto from "node:crypto";import http from "node:http";
const SECRET = process.env.ECHO_WEBHOOK_SECRET;const MAX_AGE = 300; // másodpercconst seen = new Set(); // élesben: adatbázistábla egyedi kulccsal
function verify(rawBody, signature, timestamp) { const prefix = "sha256="; if (typeof signature === "string" && signature.startsWith(prefix)) { const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest(); const given = Buffer.from(signature.slice(prefix.length), "hex"); const age = Math.abs(Date.now() / 1000 - Number(timestamp)); return given.length === expected.length && crypto.timingSafeEqual(given, expected) && age <= MAX_AGE; } return false;}
http.createServer((req, res) => { const chunks = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => { const raw = Buffer.concat(chunks); if (verify(raw, req.headers["x-upmail-signature"], req.headers["x-upmail-timestamp"]) === false) { res.writeHead(401).end(); return; } const delivery = req.headers["x-upmail-delivery"]; if (seen.has(delivery) === false) { seen.add(delivery); const body = JSON.parse(raw.toString("utf8")); for (const event of body.events) { console.log(event.type, event.subscriber?.email); } } res.writeHead(200).end(); });}).listen(3000);Express esetén ezen az útvonalon a törzset az express.raw({ type: "application/json" }) olvassa be, hogy a nyers bájtokat ellenőrizd, ne egy újra sorosított objektumot.
PHP
<?php$secret = getenv('ECHO_WEBHOOK_SECRET');$maxAge = 300; // másodperc
$raw = file_get_contents('php://input');$signature = $_SERVER['HTTP_X_UPMAIL_SIGNATURE'] ?? '';$timestamp = (int) ($_SERVER['HTTP_X_UPMAIL_TIMESTAMP'] ?? 0);$delivery = $_SERVER['HTTP_X_UPMAIL_DELIVERY'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);if (hash_equals($expected, $signature) === false || abs(time() - $timestamp) > $maxAge) { http_response_code(401); exit;}
// Szűrd a duplikátumokat a $delivery alapján (például egyedi kulccsal az adatbázisban), aztán:$body = json_decode($raw, true);foreach ($body['events'] as $event) { error_log($event['type'] . ' ' . ($event['subscriber']['email'] ?? ''));}http_response_code(200);Python (standard könyvtár)
import hashlibimport hmacimport jsonimport osimport timefrom http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["ECHO_WEBHOOK_SECRET"].encode("utf-8")MAX_AGE = 300 # másodpercseen = set() # élesben: adatbázistábla egyedi kulccsal
def verify(raw: bytes, signature: str, timestamp: str) -> bool: if not signature or not signature.startswith("sha256="): return False expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, signature[7:]): return False try: return abs(time.time() - int(timestamp)) <= MAX_AGE except (TypeError, ValueError): return False
class Handler(BaseHTTPRequestHandler): def do_POST(self): raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) if not verify(raw, self.headers.get("X-Upmail-Signature"), self.headers.get("X-Upmail-Timestamp")): self.send_response(401) self.end_headers() return delivery = self.headers.get("X-Upmail-Delivery") if delivery not in seen: seen.add(delivery) for event in json.loads(raw)["events"]: print(event["type"], event.get("subscriber", {}).get("email")) self.send_response(200) self.end_headers()
HTTPServer(("", 3000), Handler).serve_forever()Flask esetén a nyers törzs a request.get_data().
curl: aláírt tesztkérés a saját végpontodra
Ez ugyanúgy állítja össze a kérést, mint az Echo, így a fogadódat a bekötés előtt kipróbálhatod:
export ECHO_WEBHOOK_SECRET=<az aláírókulcs>BODY='{"delivery_id":"3b0f6a52-8c1e-4d7a-9f20-6e4b1c2d3a45","webhook_id":"8c17fb2c-011a-4058-a958-b56459fb2c4c","sent_at":"2026-10-22T06:15:02Z","events":[{"type":"subscriber.created","occurred_at":"2026-10-22T06:14:58.112623Z","subscriber":{"id":"d5f0b4b1-b59d-40eb-b2e0-74857893a055","email":"[email protected]","status":"active"}}]}'TS=$(date +%s)SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$ECHO_WEBHOOK_SECRET" -hex | sed 's/^.* //')curl -i -X POST "http://localhost:3000/" \ -H "Content-Type: application/json" \ -H "X-Upmail-Signature: sha256=$SIG" \ -H "X-Upmail-Delivery: 3b0f6a52-8c1e-4d7a-9f20-6e4b1c2d3a45" \ -H "X-Upmail-Timestamp: $TS" \ --data "$BODY"A fenti három fogadó erre a kérésre 200 választ ad, az ismétlésre szintén 200-at (másodszor nem dolgozza fel), rossz kulcsnál pedig 401-et.
Válasz, újrapróbálás és duplikátumszűrés
Szekció neve “Válasz, újrapróbálás és duplikátumszűrés”- Válaszolj bármilyen 2xx kóddal 10 másodpercen belül. Az Echo legfeljebb 64 KB-ot olvas a válaszodból, és az elejét a kézbesítési naplóban tartja; a tartalma nem számít.
- Minden más (más kód, időtúllépés, kapcsolódási hiba, átirányítás) sikertelen kísérletnek számít. Az Echo 10, 100 és 1000 másodperc múlva újra próbálkozik: összesen négy kísérlet. A negyedik után a kézbesítés sikertelen.
- A sikertelen kézbesítés a felületről újraküldhető (Újraküldés a kézbesítés ablakában). Az újraküldés új kézbesítés: saját
delivery_idértéke van, aresent_frompedig megnevezi az eredetit, amely változatlan marad. - A kézbesítés legalább egyszeri: ugyanaz a kézbesítés többször is megérkezhet (például ha a 2xx válaszod elveszett). Tárold az
X-Upmail-Deliveryértékét, és a már feldolgozott azonosítókat hagyd ki. A kézi Újraküldés új azonosítót kap, így a rendszered feldolgozza. - A lassú munkát a válasz után végezd (tedd az eseményeket egy sorba), hogy beleférj a 10 másodpercbe.
- A webhook kikapcsolása a várakozó kézbesítéseit sikertelenné teszi. A napló 30 napig őrzi a kézbesítéseket.
Célcím-szabályok
Szekció neve “Célcím-szabályok”Az Echo csak olyan célt hív, amely megfelel ezeknek:
- Az URL
http://vagyhttps://kezdetű, és legfeljebb 2048 karakter. Élesben HTTPS-t használj. - A port 80, 443, vagy 1024 fölötti.
- A hosztnévnek csak nyilvános internetes címekre szabad mutatnia. A privát, loopback, link-local, szolgáltatói NAT, multicast, dokumentációs és más fenntartott tartományokat az Echo elutasítja, akkor is, ha egy nyilvános név ilyenre oldódik fel. Az elutasított cím azonnal sikertelenné teszi a kézbesítést, újrapróbálás nélkül.
- Nincs proxy és nincs átirányítás: a 3xx válasz sikertelen kísérletnek számít.
A portot és az URL formáját mentéskor, a címet minden kézbesítéskor ellenőrzi az Echo.
Webhookok kezelése az API-n át
Szekció neve “Webhookok kezelése az API-n át”Olvasáshoz Beállítások: megtekintés, módosításhoz Beállítások: kezelés jog kell. Beállítások: kezelés jog nélkül a token az URL-nek csak az eredetét látja.
| Metódus és útvonal | Mit csinál |
|---|---|
GET /webhooks |
Listázza a webhookokat (limit, cursor). |
POST /webhooks |
Webhookot hoz létre. Az aláírókulcs csak ebben a válaszban jelenik meg. |
GET /webhooks/{id} |
Egy webhook. |
PUT /webhooks/{id} |
A megadott kulcsokat módosítja. |
DELETE /webhooks/{id} |
Törli a kézbesítési naplójával együtt. |
Törzskulcsok: name (legfeljebb 100 karakter), url, events (legalább egy), enabled (alapból true), batchable (alapból false).
curl -X POST "$ECHO_API/webhooks" \ -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \ -d '{"name":"CRM szinkron","url":"https://crm.example.com/hooks/echo","events":["subscriber.created","subscriber.unsubscribed"],"enabled":false}'{"data":{"id":"27ff1d12-18c0-4897-9c90-330128f2d038","name":"CRM szinkron","url":"https://crm.example.com/hooks/echo","events":["subscriber.created","subscriber.unsubscribed"],"enabled":false,"batchable":false,"created_at":"2026-10-22T08:52:10.281979Z","updated_at":"2026-10-22T08:52:10.281979Z","last_delivery":null,"secret":"<signing-secret>"}}(Az aláírókulcs helyén példaérték áll.)
curl -X PUT "$ECHO_API/webhooks/27ff1d12-18c0-4897-9c90-330128f2d038" \ -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \ -d '{"events":["subscriber.created","subscriber.unsubscribed","campaign.click"],"batchable":true}'{"data":{"id":"27ff1d12-18c0-4897-9c90-330128f2d038","name":"CRM szinkron","url":"https://crm.example.com/hooks/echo","events":["subscriber.created","subscriber.unsubscribed","campaign.click"],"enabled":false,"batchable":true,"created_at":"2026-10-22T08:52:10.281979Z","updated_at":"2026-10-22T08:52:10.31774Z","last_delivery":null}}A last_delivery a legutóbbi kézbesítés status (pending, delivered, failed), status_code és időpont értéke, vagy null. Az eseményeket az Echo rögzített sorrendben tárolja, bármilyen sorrendben küldöd őket.
| Hiba | Mikor |
|---|---|
422, fields.url: Enter an http:// or https:// URL. |
Nem http(s) URL. |
422, fields.url: Use port 80, 443 or a port above 1024. |
Más port. |
422, fields.events és fields.events.<n> |
Ismeretlen eseménynév. |
422, fields.batchable |
Csak kötegelve küldhető esemény "batchable": true nélkül. |
409 webhook_limit |
A munkaterületnek már 20 webhookja van. |
Az aláírókulcs forgatása, a tesztkézbesítés és a sikertelen kézbesítések újraküldése csak a felületen érhető el. Kikapcsolt webhooknak a felület nem küld tesztet.
Munkafolyamatok webhook-lépései
Szekció neve “Munkafolyamatok webhook-lépései”Egy automatizálás webhook-lépéssel is hívhatja a végpontodat. Ezek a kérések ugyanazokat a fejléceket és ugyanazt az aláírási módot használják, de:
- A munkaterület workflow-kulcsa írja alá őket (felület: Integrációk, Webhookok, Workflow-kulcs), nem egy webhook kulcsa.
- A törzs egyetlen objektum:
{ "event": "automation.webhook", "delivery": "5d1c9b7e-2f4a-4c3b-8e6d-1a2b3c4d5e6f:3", "automation": {"id": "71a06a41-7c5f-4ab7-b865-b33899fa7343", "name": "Üdvözlő sorozat (HU)"}, "step": "w1", "subscriber": {"id": "d5f0b4b1-b59d-40eb-b2e0-74857893a055", "email": "[email protected]", "status": "active", "fields": {"name": "Lilla", "city": "Szeged"}}, "sent_at": "2026-10-22T06:20:00Z"}- A
delivery(azX-Upmail-Deliveryfejlécben is) a futást és a lépést azonosítja: erre szűrd a duplikátumokat. - A nem 2xx választ kapó lépést az Echo összesen legfeljebb ötször próbálja, rövid, növekvő várakozással. Tiltott célcímet vagy portot nem próbál újra.
- A lépés kimarad, ha a feliratkozó a futás idejére már nem aktív (kivéve közvetlenül a munkafolyamat saját leiratkoztató lépése után).
Ugyanazok a célcím-szabályok érvényesek.