Návod
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é 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.
- 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.
{
"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 získáte zpracovanou zprávu:
{
"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, Výpis příloh, nebo Načtení surového MIME pro původní zdroj.
Vytvořte webhook
-
Přidejte webhook. Přejděte do Email APIWebhooks, vyberte Add webhook, zadejte název a URL svého endpointu a vyberte Create.
-
Zkopírujte tajný klíč. Dialogové okno jednou zobrazí tajný klíč webhooku (
whsec_…). Uložte ho do prostředí své aplikace jakoEMAILIT_WEBHOOK_SECRET. -
Odebírejte jen email.received. Na kartě Settings webhooku vypněte All events, v sekci Emails vyberte
email.receiveda vyberte Save.
Zavolejte endpoint Vytvoření webhooku se seznamem událostí. Odpověď obsahuje secret.
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, 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, projde pole, přeskočí vše, co není email.received nebo co už bylo zpracováno, a načte každou zprávu.
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);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
$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ěď
2xxaž 30 sekund. Vypršení časového limitu, jiný stav než2xxnebo přesměrování se počítá jako selhání a celá dávka se zopakuje podle plánu opakování. Pokud načtení a zpracování může trvat déle, uložte události do fronty, vraťte200a 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_idzaznamenejte 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.
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:
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
- Pošlete zprávu na adresu na své příchozí subdoméně.
- Na kartě Requests webhooku má požadavek
email.receivedstav Delivered. - 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í.