# Zpracování příchozích e-mailů webhooky

> Přihlaste webhook k odběru email.received, ověřte každý požadavek a pak přes API načtěte tělo, hlavičky a přílohy každé přijaté zprávy.

Tento návod ukazuje, jak na přijatou poštu reagovat ve vlastním kódu. Emailit pošle na váš endpoint událost `email.received` a váš kód přes API načte celou zprávu. Příklady ověřují podpis, zpracovávají dávky událostí, stahují přílohy a přeskakují duplicity.

## Než začnete

- [Příchozí e-maily jsou nastavené](/cs/docs/inbound/set-up/) a testovací zpráva se zobrazuje na kartě **Incoming**.
- API klíč s oprávněním **Full Access**. Klíče Sending Only obsah e-mailů číst nesmějí. Viz [API klíče](/cs/docs/developers/api-keys/).
- Veřejný endpoint s HTTPS, který přijímá požadavky `POST`. Pro lokální vývoj použijte tunel, například ngrok nebo Cloudflare Tunnel.

## Jak celý proces funguje

Webhook vám oznámí, že zpráva dorazila. Neobsahuje tělo ani přílohy, takže požadavky zůstávají malé a obsah načítáte, jen když ho potřebujete.

```json title="email.received (jedna událost v poli požadavku)"
{
  "event_id": "evt_2xGk9Tb4QmF6wN2cJpR7uZsE1kD",
  "type": "email.received",
  "object": {
    "id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
    "object": "email",
    "from": "ada@example.com",
    "to": "support@inbound.acme.com",
    "subject": "Question about order 1042",
    "created_at": "2026-10-01T09:14:05.317000+00:00"
  },
  "data": {
    "object": {
      "id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
      "object": "email",
      "from": "ada@example.com",
      "to": "support@inbound.acme.com",
      "subject": "Question about order 1042",
      "created_at": "2026-10-01T09:14:05.317000+00:00"
    }
  }
}
```

Pomocí `data.object.id` a endpointu [Načtení e-mailu](/cs/docs/api-reference/emails/get/) získáte zpracovanou zprávu:

```json title="GET /v2/emails/em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA (zkráceno)"
{
  "object": "email",
  "id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
  "type": "inbound",
  "status": "received",
  "from": "ada@example.com",
  "to": "support@inbound.acme.com",
  "subject": "Question about order 1042",
  "message_id": "<CAF3x1@mail.example.com>",
  "headers": {
    "From": "Ada Lovelace <ada@example.com>",
    "Reply-To": "ada@example.com",
    "Date": "Thu, 01 Oct 2026 09:14:02 +0000"
  },
  "body": {
    "text": "Hi, my order hasn't arrived yet...",
    "html": "<p>Hi, my order hasn't arrived yet...</p>"
  },
  "attachments": [
    {
      "filename": "receipt.pdf",
      "content_type": "application/pdf",
      "size": 48213,
      "content_id": null,
      "content_disposition": "attachment",
      "content": "JVBERi0xLjcKJcfsj6IK..."
    }
  ]
}
```

`content` přílohy je kódovaný v Base64. Názvy hlaviček si zachovávají původní velikost písmen, a když se hlavička vyskytuje víckrát (například `Received`), zůstane jen poslední hodnota. Pokud potřebujete jen část zprávy, použijte užší endpointy: [Načtení těla e-mailu](/cs/docs/api-reference/emails/body/), [Výpis příloh](/cs/docs/api-reference/emails/attachments/), nebo [Načtení surového MIME](/cs/docs/api-reference/emails/raw/) pro původní zdroj.

## Vytvořte webhook

**Webové rozhraní**

  1. **Přidejte webhook.** Přejděte do **Email API → Webhooks**, vyberte **Add webhook**, zadejte název a URL svého endpointu a vyberte **Create**.

  2. **Zkopírujte tajný klíč.** Dialogové okno jednou zobrazí tajný klíč webhooku (`whsec_…`). Uložte ho do prostředí své aplikace jako `EMAILIT_WEBHOOK_SECRET`.

  3. **Odebírejte jen email.received.** Na kartě **Settings** webhooku vypněte **All events**, v sekci **Emails** vyberte `email.received` a vyberte **Save**.

**API**

  Zavolejte endpoint [Vytvoření webhooku](/cs/docs/api-reference/webhooks/create/) se seznamem událostí. Odpověď obsahuje `secret`.

```bash
curl https://api.emailit.com/v2/webhooks \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Inbound mail",
    "url": "https://acme.com/webhooks/emailit",
    "events": ["email.received"]
  }'
```

V tarifech Pro, Business a Custom můžete přidat [filtr obsahu](/cs/docs/webhooks/set-up/#filter-events-by-payload), aby webhook dostával poštu jen pro některé adresy, například když `to` končí na `@inbound.acme.com`.

## Napište obsluhu

Tělo každého požadavku je pole JSON až se 100 událostmi. Obsluha níže ověří [podpis](/cs/docs/webhooks/request-signature/), projde pole, přeskočí vše, co není `email.received` nebo co už bylo zpracováno, a načte každou zprávu.

**Node.js**

```javascript title="server.js"
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const WEBHOOK_SECRET = process.env.EMAILIT_WEBHOOK_SECRET; // whsec_...
const API_KEY = process.env.EMAILIT_API_KEY; // secret_..., Full Access
const processed = new Set(); // use a database table with a unique key in production

function verify(rawBody, signature, timestamp) {
  if (!signature || !timestamp) return false;
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!(age <= 300)) return false; // 5-minute tolerance
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

async function fetchEmail(id) {
  const res = await fetch(`https://api.emailit.com/v2/emails/${id}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  if (!res.ok) throw new Error(`Emailit API returned ${res.status}`);
  return res.json();
}

async function handleReceived(event) {
  const email = await fetchEmail(event.data.object.id);
  console.log(`From ${email.from} to ${email.to}: ${email.subject}`);
  console.log(email.body.text ?? email.body.html);

  for (const file of email.attachments ?? []) {
    const bytes = Buffer.from(file.content, 'base64');
    console.log(`Attachment ${file.filename} (${file.content_type}, ${bytes.length} bytes)`);
  }
}

// Keep the raw body: the signature is computed over the exact bytes.
app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verify(req.body, req.get('X-Emailit-Signature'), req.get('X-Emailit-Timestamp'))) {
    return res.status(401).send('Invalid signature');
  }

  const events = JSON.parse(req.body.toString('utf8'));
  try {
    for (const event of events) {
      if (event.type !== 'email.received' || processed.has(event.event_id)) continue;
      await handleReceived(event);
      processed.add(event.event_id);
    }
    res.sendStatus(200);
  } catch (err) {
    console.error(err);
    res.sendStatus(500); // Emailit retries the whole batch later
  }
});

app.listen(3000);
```

**Python**

```python title="app.py"
import base64
import hashlib
import hmac
import json
import os
import time

import requests
from flask import Flask, abort, request

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["EMAILIT_WEBHOOK_SECRET"].encode()  # whsec_...
API_KEY = os.environ["EMAILIT_API_KEY"]  # secret_..., Full Access
processed = set()  # use a database table with a unique key in production

def verify(raw_body: bytes, signature: str | None, timestamp: str | None) -> bool:
    if not signature or not timestamp:
        return False
    try:
        if abs(time.time() - int(timestamp)) > 300:  # 5-minute tolerance
            return False
    except ValueError:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = hmac.new(WEBHOOK_SECRET, signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

def fetch_email(email_id: str) -> dict:
    response = requests.get(
        f"https://api.emailit.com/v2/emails/{email_id}",
        headers={"Authorization": f"Bearer {API_KEY}"},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

def handle_received(event: dict) -> None:
    email = fetch_email(event["data"]["object"]["id"])
    print(f"From {email['from']} to {email['to']}: {email['subject']}")
    print(email["body"]["text"] or email["body"]["html"])

    for attachment in email.get("attachments") or []:
        data = base64.b64decode(attachment["content"])
        print(f"Attachment {attachment['filename']} ({len(data)} bytes)")

@app.post("/webhooks/emailit")
def emailit_webhook():
    raw_body = request.get_data()  # raw bytes, before any JSON parsing
    if not verify(
        raw_body,
        request.headers.get("X-Emailit-Signature"),
        request.headers.get("X-Emailit-Timestamp"),
    ):
        abort(401)

    for event in json.loads(raw_body):
        if event["type"] != "email.received" or event["event_id"] in processed:
            continue
        handle_received(event)  # an exception returns 500 and Emailit retries
        processed.add(event["event_id"])

    return "", 200
```

**PHP**

```php title="webhook.php"
<?php
$webhookSecret = getenv('EMAILIT_WEBHOOK_SECRET'); // whsec_...
$apiKey = getenv('EMAILIT_API_KEY'); // secret_..., Full Access

// Dedupe table: CREATE TABLE processed_events (event_id TEXT PRIMARY KEY)
$db = new PDO('sqlite:' . __DIR__ . '/events.sqlite');

$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EMAILIT_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_EMAILIT_TIMESTAMP'] ?? '';

$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $webhookSecret);
$fresh = ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300;
if (!$fresh || !hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

function fetchEmail(string $id, string $apiKey): array
{
    $ch = curl_init('https://api.emailit.com/v2/emails/' . rawurlencode($id));
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer {$apiKey}"],
        CURLOPT_TIMEOUT => 10,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status !== 200) {
        throw new RuntimeException("Emailit API returned {$status}");
    }
    return json_decode($body, true);
}

try {
    foreach (json_decode($rawBody, true) as $event) {
        if ($event['type'] !== 'email.received') {
            continue;
        }
        $seen = $db->prepare('SELECT 1 FROM processed_events WHERE event_id = ?');
        $seen->execute([$event['event_id']]);
        if ($seen->fetchColumn()) {
            continue;
        }

        $email = fetchEmail($event['data']['object']['id'], $apiKey);
        error_log("From {$email['from']} to {$email['to']}: {$email['subject']}");
        foreach ($email['attachments'] ?? [] as $attachment) {
            $bytes = base64_decode($attachment['content']);
            file_put_contents(sys_get_temp_dir() . '/' . basename($attachment['filename']), $bytes);
        }

        $db->prepare('INSERT INTO processed_events (event_id) VALUES (?)')
           ->execute([$event['event_id']]);
    }
    http_response_code(200);
} catch (Throwable $e) {
    error_log($e->getMessage());
    http_response_code(500); // Emailit retries the whole batch later
}
```

## Ošetřete opakování a duplicity

- **Odpovězte do 30 sekund.** Emailit čeká na odpověď `2xx` až 30 sekund. Vypršení časového limitu, jiný stav než `2xx` nebo přesměrování se počítá jako selhání a celá dávka se zopakuje podle [plánu opakování](/cs/docs/webhooks/retries-and-failures/). Pokud načtení a zpracování může trvat déle, uložte události do fronty, vraťte `200` a zpracujte je v úloze na pozadí.
- **Odstraňujte duplicity podle `event_id`.** Opakovaná dávka znovu obsahuje všechny své události, včetně těch, které jste před selháním už zpracovali. Po zpracování si každé `event_id` zaznamenejte a ID, která jste už viděli, přeskakujte. Každá přijatá zpráva má navíc vlastní ID e-mailu, které můžete použít jako druhý klíč.
- **Potvrzujte i události, které nezpracováváte.** Vraťte `2xx`, i když dávka obsahuje jen typy událostí, které ignorujete. Jinak je Emailit bude opakovat.
- **Načítejte obsah včas.** Obsah zpráv se uchovává omezenou dobu, která závisí na tarifu (7 dní v tarifu Pay as you go). Potom API vrací e-mail bez těla a příloh. Viz [Uchovávání dat](/cs/docs/data-retention/).

## Směrujte zprávy podle adresy

Protože se přijímá jakákoli část adresy před zavináčem, můžete do adresy zakódovat informace a přečíst je zpět z `to`. Posílejte například oznámení s `reply_to` nastaveným na `reply+4821@inbound.acme.com` a odpovědi pak směrujte k tiketu 4821:

```javascript
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
  await addReplyToTicket(match[1], email.body.text);
}
```

## Ověřte, že vše funguje

1. Pošlete zprávu na adresu na své příchozí subdoméně.
2. Na kartě **Requests** webhooku má požadavek `email.received` stav **Delivered**.
3. Vaše aplikace zaloguje odesílatele, předmět a případné přílohy.

Pokud má požadavek stav **Attempting** nebo **Failed**, vyberte **View** v řádku s neúspěšným požadavkem a uvidíte stavový kód a tělo odpovědi, které váš endpoint vrátil. Viz [Opakování a selhání](/cs/docs/webhooks/retries-and-failures/).

## Související

  - [Ověření podpisu webhooků](/cs/docs/webhooks/request-signature/)
  - [Formát požadavků webhooků](/cs/docs/webhooks/webhook-requests/)
  - [Přeposílání automatizacemi](/cs/docs/inbound/forward-with-automations/)
  - [Načtení e-mailu](/cs/docs/api-reference/emails/get/)

---
Zdroj: https://emailit.com/cs/docs/inbound/process-with-webhooks/
