Webhooks
Receba um aviso no seu servidor a cada mudança de status de uma correspondência ou verificação.
Um webhook é uma URL do seu servidor que a CartaFlow chama quando algo acontece na sua conta. A cada mudança de status de uma correspondência ou de uma verificação, enviamos um POST com o evento no corpo. Assim o seu sistema fica sabendo que uma correspondência foi postada, ou que um endereço foi verificado, sem consultar a API de tempos em tempos.
Configurar
Crie um endpoint no seu servidor que aceite POST com corpo JSON e responda rápido. A URL precisa usar HTTPS.
Cadastre a URL no painel, em Desenvolvedores > Webhooks, e escolha os eventos que quer receber.
Copie o segredo de assinatura. Cada webhook tem um segredo que começa com whsec_, mostrado no painel. Guarde em uma variável de ambiente, como CARTAFLOW_WEBHOOK_SECRET, e use para conferir a assinatura de cada entrega.
O que chega no seu servidor
O corpo é um objeto event. Em data vem o objeto inteiro, como estava logo depois do acontecimento, então você não precisa consultar a API depois:
{
"id": "evt_6Tq1Yz8Wm3Rb",
"object": "event",
"type": "letter.posted",
"data": {
"id": "ltr_8Kq2wX4mN7pR",
"object": "letter",
"status": "posted",
"to": {
"name": "Destinatário Exemplo",
"street": "Rua Exemplo",
"number": "128",
"complement": "Ap. 52",
"neighborhood": "Centro",
"city": "São Paulo",
"state": "SP",
"postal_code": "01001-000"
},
"reference": "fatura-2026-10-0042",
"metadata": { "customer_id": "1842" },
"status_transitions": {
"received_at": "2026-10-13T14:20:00Z",
"in_production_at": "2026-10-13T21:00:00Z",
"printed_at": "2026-10-14T10:42:00Z",
"posted_at": "2026-10-14T16:05:00Z",
"canceled_at": null,
"failed_at": null
},
"created_at": "2026-10-13T14:20:00Z"
},
"created_at": "2026-10-14T16:05:00Z"
}Para caber na página, o exemplo mostra só alguns campos de data. Na entrega real, data traz a correspondência completa, igual à resposta de GET /v1/letters/{letter_id}.
Cada entrega vem com três cabeçalhos:
| Cabeçalho | O que é |
|---|---|
webhook-id | O id do evento, igual a id do corpo. É o mesmo em todas as tentativas de entrega do mesmo evento. |
webhook-timestamp | O momento do envio, em segundos Unix. |
webhook-signature | A assinatura do corpo, no formato v1,<assinatura>. Pode trazer mais de uma, separadas por espaço, durante a troca de um segredo. |
Conferir a assinatura
Confira a assinatura antes de confiar no conteúdo. Ela prova que a entrega veio da CartaFlow e que o corpo não foi alterado. A CartaFlow segue a especificação Standard Webhooks, que tem bibliotecas oficiais para várias linguagens.
Use o corpo cru, exatamente como chegou. Se o seu framework converter o JSON e você gerar o texto de novo, a assinatura não confere.
// npm install standardwebhooks express
import express from 'express';
import { Webhook } from 'standardwebhooks';
const app = express();
const webhook = new Webhook(process.env.CARTAFLOW_WEBHOOK_SECRET);
// express.raw mantém o corpo exatamente como chegou.
app.post('/webhooks/cartaflow', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch {
return res.status(400).send('Assinatura inválida');
}
res.status(200).end(); // responda primeiro
queue.add(event); // processe depois, fora da requisição
});A conta é esta:
- Monte o texto assinado:
{webhook-id}.{webhook-timestamp}.{corpo cru}, com pontos entre as partes. - Pegue o segredo sem o prefixo
whsec_e decodifique de base64. O resultado é a chave. - Calcule o HMAC-SHA256 do texto assinado com essa chave e codifique o resultado em base64.
- Compare com cada item
v1,<assinatura>do cabeçalhowebhook-signature, usando uma comparação de tempo constante. Se algum for igual, a assinatura confere. - Recuse a entrega se
webhook-timestampestiver a mais de 5 minutos do seu relógio. Isso impede que alguém reenvie uma entrega antiga.
import crypto from 'node:crypto';
export function verifyCartaflowWebhook(rawBody, headers, secret) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'];
if (!id || !timestamp || !signatures) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 5 * 60) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
return signatures.split(' ').some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'base64');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}Responda rápido e processe depois
Responda com qualquer status 2xx em até 15 segundos. Esse é o sinal de que você recebeu o evento. Faça o trabalho demorado, como atualizar o cadastro ou mandar um e-mail, depois de responder, por exemplo em uma fila.
Qualquer outra resposta, ou nenhuma resposta a tempo, faz a CartaFlow tentar de novo:
| Tentativa | Quando |
|---|---|
| 1 | Na hora do evento |
| 2 | 5 segundos depois da anterior |
| 3 | 5 minutos depois |
| 4 | 30 minutos depois |
| 5 | 2 horas depois |
| 6 | 5 horas depois |
| 7 | 10 horas depois |
| 8 | 10 horas depois |
Depois da última tentativa, o evento não é enviado de novo. Ele continua disponível em GET /v1/events por 30 dias.
Repetições e ordem
- O mesmo evento pode chegar mais de uma vez. Uma entrega pode ser repetida mesmo que o seu servidor tenha recebido, por exemplo se a resposta se perder no caminho. Guarde o
webhook-iddos eventos processados e ignore os repetidos. - Os eventos podem chegar fora de ordem. Um
letter.printedpode chegar depois de umletter.posted. Não deduza o status pela ordem de chegada: use ostatuse ostatus_transitionsdo objeto emdata, que são uma foto do momento do evento. Para ter certeza do estado atual, consulte o objeto pela API.
Recuperar eventos perdidos
Se o seu servidor ficou fora do ar, busque o que perdeu em GET /v1/events, filtrando por created_after. Para ver a história de uma correspondência, use object_id. Os eventos ficam guardados por 30 dias e são os mesmos que os webhooks entregam.
Evento de teste
No painel, em Desenvolvedores > Webhooks, o botão Enviar evento de teste envia para o seu endpoint uma entrega do tipo webhook.test. Use para conferir que a URL responde, que a assinatura confere e que o seu servidor responde 2xx a tempo.
- A entrega tem os mesmos cabeçalhos e a mesma assinatura das outras, com o segredo
whsec_do webhook. - O corpo segue o formato de qualquer evento, com
type: "webhook.test"e um objeto de exemplo emdata. Esse objeto não existe na sua conta: não grave nada a partir dele. - O evento de teste é enviado uma vez, sem novas tentativas, e nunca aparece em
GET /v1/events.
Durante o desenvolvimento, a CartaFlow precisa alcançar a sua URL pela internet. Use um túnel, como ngrok ou Cloudflare Tunnel, para expor o seu servidor local com uma URL HTTPS, cadastre essa URL no painel e envie um evento de teste.
Eventos
Cada mudança de status gera um evento com o nome objeto.novo_status. Quando uma correspondência é alterada no painel antes da produção, sem mudar de status (a data de envio ou o endereço, por exemplo), o evento é letter.updated, com a correspondência como ficou.
| Evento | Quando é enviado |
|---|---|
letter.scheduled | Uma correspondência foi agendada com send_date. Ainda não foi cobrada. |
letter.received | Uma correspondência foi cobrada e entrou na fila de produção: logo depois de criada ou, se foi agendada, na data do envio. Vale para correspondências criadas pela API e pelo painel. |
letter.in_production | A correspondência entrou em um lote de impressão e não pode mais ser cancelada. |
letter.printed | A correspondência foi impressa e envelopada. |
letter.posted | A correspondência foi entregue aos Correios. É o último evento de uma correspondência enviada. |
letter.canceled | A correspondência foi cancelada. Veja cancellation_reason: requested quando você cancelou, insufficient_balance quando uma correspondência agendada não pôde ser cobrada na data. |
letter.failed | A correspondência falhou na produção e não será postada. O valor voltou para o saldo automaticamente. Veja failure_reason. |
letter.updated | A correspondência foi alterada no painel antes da produção, sem mudar de status: data de envio, endereço, arquivo, variáveis, impressão, serviços dos Correios ou referência. data traz a correspondência como ficou. Se a mudança alterou o preço de uma correspondência agendada, price traz o novo valor. |
verification.received | Uma verificação foi criada, pela API ou pelo painel (veja source), e a correspondência com o código está na fila de produção. |
verification.in_production | A correspondência com o código está sendo impressa. A partir daqui, cancelar revoga a verificação, sem devolução. |
verification.awaiting_code | A correspondência com o código foi postada. A pessoa já pode confirmar quando receber. |
verification.verified | A pessoa confirmou o código certo. O endereço está verificado. |
verification.failed | Foram digitados 5 códigos errados e a verificação foi bloqueada. |
verification.expired | A pessoa não confirmou o código em 60 dias desde a postagem. |
verification.canceled | A verificação foi cancelada antes da postagem, por você ou pela CartaFlow, e o valor voltou para o saldo. |
verification.revoked | Você cancelou a verificação depois que a produção começou. O código deixou de funcionar e o valor não volta para o saldo. Se ela estava verified, trate o endereço como não verificado. |
Trate tipos desconhecidos sem erro
Responda 2xx a qualquer entrega com assinatura válida, mesmo de um tipo que o seu sistema não usa, como webhook.test, e ignore o que você não conhece. Novos tipos de evento podem surgir sem aviso. Veja Versões e compatibilidade.