Tovább a tartalomhoz
Dokumentáció ehhez: Echo
ENuptools.io
Markdown

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_at szerint 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é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.

POST /hooks/echo HTTP/1.1
Host: crm.example.com
Content-Type: application/json
User-Agent: upmail-webhook
X-Upmail-Signature: sha256=<64 hex characters>
X-Upmail-Delivery: 3b0f6a52-8c1e-4d7a-9f20-6e4b1c2d3a45
X-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.

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",
"email": "[email protected]",
"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.

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ásodperc
const 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 hashlib
import hmac
import json
import os
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
SECRET = os.environ["ECHO_WEBHOOK_SECRET"].encode("utf-8")
MAX_AGE = 300 # másodperc
seen = 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:

Terminál
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, a resent_from pedig 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.

Az Echo csak olyan célt hív, amely megfelel ezeknek:

  • Az URL http:// vagy https:// 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.

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).

Terminál
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.)

Terminál
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.

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 (az X-Upmail-Delivery fejlé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.