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": ["message.delivered", "message.bounced", "message.complained", "message.opened", "message.clicked"]
  }'

Odpowiedź:

{
  "id": "wh_abc123",
  "url": "https://twojaapka.pl/webhooks/mailingapi",
  "events": ["message.delivered", "message.bounced", "message.complained", "message.opened", "message.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
message.delivered Email zaakceptowany przez serwer odbiorcy
message.bounced Email odrzucony (permanentnie lub tymczasowo)
message.deferred Tymczasowy błąd dostarczenia, ponowimy próbę
message.opened Odbiorca otworzył email (piksel śledzący)
message.clicked Odbiorca kliknął link
message.unsubscribed Odbiorca kliknął link wypisania
message.complained Odbiorca oznaczył jako spam
quality.alert Alert jakości wysyłki (np. przekroczony próg bounce/complaint rate)

Uwaga: Dostarczanie przychodzące (inbound.received) konfigurujesz per reguła przychodząca, nie przez subskrypcję webhooka opisaną tutaj. Ma odrębny, płaski format payloadu (bez koperty id/type/data) i własne nagłówki podpisu — zobacz Zdarzenie inbound.received poniżej. Nie jest częścią kontraktu z kopertą message.*/quality.* opisanego w tym przewodniku.

Struktura zdarzenia

Wszystkie zdarzenia mają tę strukturę:

{
  "id": "evt_1234567890",
  "type": "message.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": "message.delivered",
  "data": {
    "message_id": "msg_abc123",
    "to": "user@example.com",
    "delivered_at": "2024-01-15T10:30:05Z",
    "smtp_response": "250 OK"
  }
}

Zdarzenie bounced

{
  "type": "message.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": "message.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": "message.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": "message.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 — dostarczane bezpośrednio na URL skonfigurowany w tej regule.

To zdarzenie NIE używa koperty id/type/data opisanej wyżej. Payload jest płaskim obiektem JSON, a żądanie jest podpisane własnymi nagłówkami — X-Outboxa-Signature, X-Outboxa-Timestamp, X-Outboxa-Delivery-Attempt — a nie X-MailingAPI-Signature. Ujednolicenie tego zdarzenia z kopertowym kontraktem message.*/quality.* jest śledzone osobno i wykracza poza ten przewodnik.

{
  "event": "inbound",
  "timestamp": "2024-01-20T15:30:00Z",
  "from": "nadawca@example.com",
  "to": ["support@twojadomena.pl"],
  "subject": "Pomoc z zamówieniem",
  "message_id": "<abc@example.com>",
  "text_body": "Potrzebuję pomocy z zamówieniem #12345...",
  "html_body": "<p>Potrzebuję pomocy z zamówieniem #12345...</p>",
  "headers": {
    "message-id": "<abc@example.com>",
    "date": "Mon, 20 Jan 2024 15:30:00 +0000"
  },
  "attachments": [
    {
      "filename": "zrzut.png",
      "content_type": "image/png",
      "size": 45230
    }
  ],
  "spf_result": "pass",
  "dkim_result": "pass",
  "dmarc_result": "pass",
  "sender_ip": "203.0.113.10"
}

Zawartość załączników (attachments_data) jest dołączona jako base64 tylko 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 — jest generowane raz na zdarzenie i pozostaje takie samo przy każdej ponownej próbie doręczenia, więc redeliver tego samego zdarzenia zawsze niesie ten sam id:

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) lub przekroczy timeout, ponawiamy do 5 prób łącznie, z exponential backoff i jitterem między próbami. Numer bieżącej próby jest wysyłany w nagłówku X-MailingAPI-Delivery-Attempt, dzięki czemu Twój handler może logować lub reagować na ponowienia.

Po wyczerpaniu wszystkich 5 prób, dostarczenie webhooka jest oznaczane jako nieudane.

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": ["message.delivered", "message.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