Dokumentacja / Przewodniki

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

  1. Przejdź do Ustawienia → Webhooki
  2. Kliknij Utwórz webhook
  3. Wpisz URL endpointu
  4. Wybierz zdarzenia do otrzymywania
  5. 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

  1. Wyodrębnij timestamp i podpis z nagłówka
  2. Zbuduj podpisywany payload: {timestamp}.{body_żądania}
  3. Oblicz HMAC-SHA256 używając swojego sekretu webhooka
  4. 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

  1. Przejdź do Ustawienia → Webhooki → [Twój Webhook]
  2. Kliknij Wyślij testowe zdarzenie
  3. Wybierz typ zdarzenia
  4. 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

  1. Zawsze weryfikuj podpisy — Zapobiegaj sfałszowanym żądaniom
  2. Odpowiadaj szybko — Przetwarzaj asynchronicznie, zwracaj natychmiast
  3. Obsługuj duplikaty — Webhooki mogą się powtarzać
  4. Monitoruj błędy — Ustaw alerty dla nieudanych dostarczeń
  5. Używaj HTTPS — Wysyłamy tylko na bezpieczne endpointy
  6. 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