Guia prático
Processar e-mails recebidos com webhooks
Inscreva um webhook em email.received, verifique cada requisição e obtenha com a API o corpo, os cabeçalhos e os anexos de cada mensagem recebida.
Este guia mostra como reagir a e-mails recebidos no seu próprio código. O Emailit notifica o seu endpoint com um evento email.received, e o seu código obtém a mensagem completa com a API. Os exemplos verificam a assinatura, tratam lotes de eventos, baixam os anexos e ignoram duplicatas.
Antes de começar
- O recebimento está configurado e uma mensagem de teste aparece na aba Incoming.
- Uma chave de API com Full Access. Não é possível ler o conteúdo dos e-mails com chaves Sending Only. Consulte Chaves de API.
- Um endpoint HTTPS público que aceite requisições
POST. Para desenvolvimento local, use um túnel como ngrok ou Cloudflare Tunnel.
Como o fluxo funciona
O webhook avisa que uma mensagem chegou. Ele não contém o corpo nem os anexos, o que mantém as requisições pequenas e permite que você obtenha o conteúdo apenas quando precisar.
{
"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"
}
}
}Use data.object.id com Obter um e-mail para receber a mensagem já interpretada:
{
"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..."
}
]
}O content dos anexos vem codificado em Base64. Os nomes dos cabeçalhos mantêm a capitalização original, e quando um cabeçalho aparece mais de uma vez (como Received), apenas o último valor é mantido. Se você só precisar de parte da mensagem, use os endpoints mais específicos: Obter o corpo, Listar anexos ou Obter o MIME bruto para o código-fonte original.
Criar o webhook
-
Adicione o webhook. Acesse Email APIWebhooks, selecione Add webhook, digite um nome e a URL do seu endpoint e selecione Create.
-
Copie o segredo. A caixa de diálogo mostra o segredo do webhook (
whsec_…) uma única vez. Guarde-o comoEMAILIT_WEBHOOK_SECRETno ambiente da sua aplicação. -
Inscreva-o apenas em email.received. Na aba Settings do webhook, desative All events, selecione
email.receivedem Emails e selecione Save.
Chame Criar um webhook com a lista de eventos. A resposta inclui o 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"]
}'Nos planos Pro, Business e Custom, você pode adicionar um filtro de payload para que o webhook receba apenas os e-mails de alguns endereços, por exemplo to terminando em @inbound.acme.com.
Escrever o handler
O corpo de cada requisição é um array JSON de até 100 eventos. O handler abaixo verifica a assinatura, percorre o array, ignora tudo o que não for email.received ou que já tenha sido processado e obtém cada mensagem.
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
}Tratar novas tentativas e duplicatas
- Responda em até 30 segundos. O Emailit espera até 30 segundos por uma resposta
2xx. Um timeout, um status diferente de2xxou um redirecionamento contam como falha, e o lote inteiro recebe novas tentativas conforme o cronograma de novas tentativas. Se obter e processar as mensagens puder demorar mais, guarde os eventos em uma fila, retorne200e processe-os em um job em segundo plano. - Elimine duplicatas pelo
event_id. Um lote reenviado contém de novo todos os eventos dele, incluindo os que você já tratou antes da falha. Registre cadaevent_iddepois de processá-lo e ignore os IDs que você já viu. Cada mensagem recebida também tem o seu próprio ID de e-mail, que você pode usar como segunda chave. - Confirme o recebimento dos eventos que você não trata. Retorne
2xxmesmo quando um lote contiver apenas tipos de evento que você ignora. Caso contrário, o Emailit continua tentando enviá-los de novo. - Obtenha o conteúdo logo. O conteúdo das mensagens é guardado por um tempo limitado que depende do seu plano (7 dias no Pay as you go). Depois disso, a API retorna o e-mail sem o corpo nem os anexos. Consulte Retenção de dados.
Rotear mensagens pelo endereço
Como qualquer parte local é aceita, você pode codificar informações no endereço e lê-las de volta em to. Por exemplo, envie notificações com reply_to definido como reply+4821@inbound.acme.com e depois encaminhe as respostas para o ticket 4821:
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
await addReplyToTicket(match[1], email.body.text);
}Confirmar que funcionou
- Envie uma mensagem para um endereço do seu subdomínio de recebimento.
- Na aba Requests do webhook, a requisição
email.receivedaparece como Delivered. - A sua aplicação registra no log o remetente, o assunto e os anexos, se houver.
Se a requisição aparecer como Attempting ou Failed, selecione View em uma linha com falha para ver o código de status e o corpo da resposta que o seu endpoint retornou. Consulte Novas tentativas e falhas.