Webhooki
5 min czytania
Webhooki powiadamiają Twoją aplikację w czasie rzeczywistym o zdarzeniach email — dostarczeniach, odbiciach, otwarciach, kliknięciach i innych.
Jak działają webhooki
1. Wysyłasz email przez MailingAPI
2. Email zostaje dostarczony (lub odbity, otwarty, etc.)
3. Wysyłamy żądanie POST na Twój URL webhooka
4. Twój serwer przetwarza zdarzenie
Zamiast odpytywać nasze API, otrzymujesz natychmiastowe powiadomienia.
Tworzenie webhooka
Przez Dashboard
- Przejdź do Ustawienia → Webhooki
- Kliknij Utwórz webhook
- Wpisz URL endpointu
- Wybierz zdarzenia do otrzymywania
- Kliknij Utwórz
Przez API
curl -X POST https://api.mailingapi.com/v1/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://twojaapka.pl/webhooks/mailingapi",
"events": ["delivered", "bounced", "complained", "opened", "clicked"]
}'
Odpowiedź:
{
"id": "wh_abc123",
"url": "https://twojaapka.pl/webhooks/mailingapi",
"events": ["delivered", "bounced", "complained", "opened", "clicked"],
"secret": "whsec_xyz789...",
"status": "active"
}
Ważne: Zapisz secret — będzie potrzebny do weryfikacji podpisów webhooków.
Typy zdarzeń
| Zdarzenie | Kiedy jest wysyłane |
|---|---|
delivered |
Email zaakceptowany przez serwer odbiorcy |
bounced |
Email odrzucony (permanentnie lub tymczasowo) |
deferred |
Tymczasowy błąd dostarczenia, ponowimy próbę |
opened |
Odbiorca otworzył email (piksel śledzący) |
clicked |
Odbiorca kliknął link |
unsubscribed |
Odbiorca kliknął link wypisania |
complained |
Odbiorca oznaczył jako spam |
inbound |
Otrzymano email przychodzący |
Struktura zdarzenia
Wszystkie zdarzenia mają tę strukturę:
{
"id": "evt_1234567890",
"type": "delivered",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"message_id": "msg_abc123",
"from": "hello@twojadomena.pl",
"to": "user@example.com",
"subject": "Twoje zamówienie wysłane",
"metadata": {
"user_id": "usr_123",
"order_id": "ord_456"
}
}
}
Zdarzenie delivered
{
"type": "delivered",
"data": {
"message_id": "msg_abc123",
"to": "user@example.com",
"delivered_at": "2024-01-15T10:30:05Z",
"smtp_response": "250 OK"
}
}
Zdarzenie bounced
{
"type": "bounced",
"data": {
"message_id": "msg_abc123",
"to": "invalid@example.com",
"bounce_type": "hard",
"bounce_code": "550",
"bounce_message": "User not found",
"bounced_at": "2024-01-15T10:30:02Z"
}
}
Typy odbić:
-
hard— Permanentna awaria (usuń z listy) -
soft— Tymczasowa awaria (ponowimy próbę)
Zdarzenie opened
{
"type": "opened",
"data": {
"message_id": "msg_abc123",
"to": "user@example.com",
"opened_at": "2024-01-15T11:45:00Z",
"user_agent": "Mozilla/5.0...",
"ip_address": "203.0.113.1"
}
}
Zdarzenie clicked
{
"type": "clicked",
"data": {
"message_id": "msg_abc123",
"to": "user@example.com",
"clicked_at": "2024-01-15T11:46:30Z",
"url": "https://twojadomena.pl/zamowienie/123",
"user_agent": "Mozilla/5.0...",
"ip_address": "203.0.113.1"
}
}
Zdarzenie complained
{
"type": "complained",
"data": {
"message_id": "msg_abc123",
"to": "user@example.com",
"complained_at": "2024-01-15T12:00:00Z",
"feedback_type": "abuse"
}
}
Ważne: Zawsze wypisuj użytkowników, którzy składają skargi, by chronić swoją reputację.
Zdarzenie inbound.received
Wysyłane gdy przychodzący email jest przetworzony przez regułę z akcją webhook.
{
"type": "inbound.received",
"data": {
"message_id": "im_abc123",
"route_id": "ir_xyz789",
"from": "nadawca@example.com",
"to": ["support@twojadomena.pl"],
"subject": "Pomoc z zamówieniem",
"text": "Potrzebuję pomocy z zamówieniem #12345...",
"html": "<p>Potrzebuję pomocy z zamówieniem #12345...</p>",
"headers": {
"message-id": "<abc@example.com>",
"date": "Mon, 20 Jan 2024 15:30:00 +0000"
},
"authentication": {
"spf": "pass",
"dkim": "pass",
"dmarc": "pass"
},
"spam_score": 1.2,
"attachments": [
{
"filename": "zrzut.png",
"content_type": "image/png",
"size": 45230
}
],
"received_at": "2024-01-20T15:30:00Z"
}
}
Zawartość załączników jest dołączona jako base64 jeśli reguła ma include_attachments: true w konfiguracji akcji.
Weryfikacja podpisów
Każde żądanie webhooka zawiera nagłówek z podpisem. Weryfikuj go, by upewnić się że żądanie pochodzi od MailingAPI.
Nagłówek podpisu
X-MailingAPI-Signature: t=1705315800,v1=5d2a...
Składniki:
-
t— Timestamp Unix gdy podpis został utworzony -
v1— Podpis HMAC-SHA256
Proces weryfikacji
- Wyodrębnij timestamp i podpis z nagłówka
-
Zbuduj podpisywany payload:
{timestamp}.{body_żądania} - Oblicz HMAC-SHA256 używając swojego sekretu webhooka
- Porównaj z dostarczonym podpisem
Przykład Python
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
# Parsuj nagłówek
parts = dict(p.split("=") for p in signature_header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
# Sprawdź timestamp (ochrona przed replay attacks)
if abs(time.time() - int(timestamp)) > 300: # 5 minut
return False
# Oblicz oczekiwany podpis
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Porównaj podpisy
return hmac.compare_digest(signature, expected)
# W handlerze webhooka
@app.post("/webhooks/mailingapi")
def handle_webhook(request):
signature = request.headers.get("X-MailingAPI-Signature")
if not verify_webhook(request.body, signature, WEBHOOK_SECRET):
return Response(status_code=401)
event = request.json()
# Przetwórz zdarzenie...
return Response(status_code=200)
Przykład Node.js
const crypto = require('crypto');
function verifyWebhook(payload, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parts.t;
const signature = parts.v1;
// Sprawdź timestamp
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return false;
}
// Oblicz oczekiwany podpis
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Porównaj podpisy
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// W handlerze
app.post('/webhooks/mailingapi', (req, res) => {
const signature = req.headers['x-mailingapi-signature'];
if (!verifyWebhook(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
// Przetwórz zdarzenie...
res.status(200).send('OK');
});
Obsługa webhooków
Odpowiadaj szybko
Twój endpoint powinien zwrócić 200 OK w ciągu 30 sekund. Przetwarzaj zdarzenia asynchronicznie:
@app.post("/webhooks/mailingapi")
async def handle_webhook(request):
event = request.json()
# Kolejkuj do asynchronicznego przetwarzania
await queue.enqueue("process_email_event", event)
# Zwróć natychmiast
return Response(status_code=200)
Idempotentność
Webhooki mogą być dostarczane więcej niż raz. Używaj id zdarzenia do deduplikacji:
async def process_event(event):
# Sprawdź czy już przetworzono
if await cache.exists(f"event:{event['id']}"):
return
# Przetwórz zdarzenie
await handle_event(event)
# Oznacz jako przetworzone (z TTL)
await cache.set(f"event:{event['id']}", "1", ex=86400)
Obsługa błędów
Jeśli Twój endpoint zwróci błąd (status nie-2xx), ponawiamy z exponential backoff:
| Próba | Opóźnienie |
|---|---|
| 1 | Natychmiast |
| 2 | 1 minuta |
| 3 | 5 minut |
| 4 | 30 minut |
| 5 | 2 godziny |
| … | Do 10 ponowień |
Po wyczerpaniu wszystkich ponowień, webhook jest oznaczany jako nieudany.
Konfiguracja webhooka
Aktualizacja zdarzeń
curl -X PATCH https://api.mailingapi.com/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"events": ["delivered", "bounced"]}'
Tymczasowe wyłączenie
curl -X PATCH https://api.mailingapi.com/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer $API_KEY" \
-d '{"status": "paused"}'
Usunięcie webhooka
curl -X DELETE https://api.mailingapi.com/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer $API_KEY"
Testowanie webhooków
Przez Dashboard
- Przejdź do Ustawienia → Webhooki → [Twój Webhook]
- Kliknij Wyślij testowe zdarzenie
- Wybierz typ zdarzenia
- Kliknij Wyślij
Używając narzędzi CLI
# Lokalne testowanie z ngrok
ngrok http 3000
# Potem zaktualizuj URL webhooka na URL ngrok
Dobre praktyki
- Zawsze weryfikuj podpisy — Zapobiegaj sfałszowanym żądaniom
- Odpowiadaj szybko — Przetwarzaj asynchronicznie, zwracaj natychmiast
- Obsługuj duplikaty — Webhooki mogą się powtarzać
- Monitoruj błędy — Ustaw alerty dla nieudanych dostarczeń
- Używaj HTTPS — Wysyłamy tylko na bezpieczne endpointy
- Loguj wszystko — Pomaga w debugowaniu
Rozwiązywanie problemów
Webhook nie otrzymuje zdarzeń
- Zweryfikuj czy URL jest publicznie dostępny
- Sprawdź czy certyfikat HTTPS jest ważny
- Upewnij się że firewall przepuszcza nasze IP
- Sprawdź czy status webhooka to “active”
Weryfikacja podpisu nie działa
- Upewnij się że używasz surowego body żądania
- Sprawdź czy sekret webhooka się zgadza
- Zweryfikuj tolerancję timestampu (używamy 5 minut)
Zdarzenia przychodzą z opóźnieniem
- Sprawdź czas odpowiedzi swojego serwera
- Szukaj wzorców ponowień (wskazują na wcześniejsze błędy)
- Zweryfikuj czy asynchroniczne przetwarzanie się nie blokuje
Następne kroki
- Twórz szablony dla spójnych emaili
- Skonfiguruj walidację by utrzymać higienę listy
- Monitoruj dostarczalność z analityką