# 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](/pt/docs/inbound/set-up/) 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](/pt/docs/developers/api-keys/).
- 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.

```json title="email.received (um evento no array da requisição)"
{
  "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](/pt/docs/api-reference/emails/get/) para receber a mensagem já interpretada:

```json title="GET /v2/emails/em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA (resumido)"
{
  "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](/pt/docs/api-reference/emails/body/), [Listar anexos](/pt/docs/api-reference/emails/attachments/) ou [Obter o MIME bruto](/pt/docs/api-reference/emails/raw/) para o código-fonte original.

## Criar o webhook

**Painel**

  1. **Adicione o webhook.** Acesse **Email API → Webhooks**, selecione **Add webhook**, digite um nome e a URL do seu endpoint e selecione **Create**.

  2. **Copie o segredo.** A caixa de diálogo mostra o segredo do webhook (`whsec_…`) uma única vez. Guarde-o como `EMAILIT_WEBHOOK_SECRET` no ambiente da sua aplicação.

  3. **Inscreva-o apenas em email.received.** Na aba **Settings** do webhook, desative **All events**, selecione `email.received` em **Emails** e selecione **Save**.

**API**

  Chame [Criar um webhook](/pt/docs/api-reference/webhooks/create/) com a lista de eventos. A resposta inclui o `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"]
  }'
```

Nos planos Pro, Business e Custom, você pode adicionar um [filtro de payload](/pt/docs/webhooks/set-up/#filter-events-by-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](/pt/docs/webhooks/request-signature/), percorre o array, ignora tudo o que não for `email.received` ou que já tenha sido processado e obtém cada mensagem.

**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
}
```

## 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 de `2xx` ou um redirecionamento contam como falha, e o lote inteiro recebe novas tentativas conforme o [cronograma de novas tentativas](/pt/docs/webhooks/retries-and-failures/). Se obter e processar as mensagens puder demorar mais, guarde os eventos em uma fila, retorne `200` e 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 cada `event_id` depois 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 `2xx` mesmo 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](/pt/docs/data-retention/).

## 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:

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

## Confirmar que funcionou

1. Envie uma mensagem para um endereço do seu subdomínio de recebimento.
2. Na aba **Requests** do webhook, a requisição `email.received` aparece como **Delivered**.
3. 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](/pt/docs/webhooks/retries-and-failures/).

## Veja também

  - [Verificar assinaturas de webhook](/pt/docs/webhooks/request-signature/)
  - [Formato das requisições de webhook](/pt/docs/webhooks/webhook-requests/)
  - [Encaminhar com automações](/pt/docs/inbound/forward-with-automations/)
  - [Obter um e-mail](/pt/docs/api-reference/emails/get/)

---
Fonte: https://emailit.com/pt/docs/inbound/process-with-webhooks/
