Webhooks
O formato único de webhook da Asender (v1): cabeçalhos, assinatura, verificadores em Node, Python, Go e PHP, cadastro por emissor e API /v1/webhooks, tentativas e idempotência.
- Visão geral
- O formato v1
- Verificadores (v1)
- 1. Eventos da conta
- 2. Destinos de lead do CRM
- 3. Destino "webhook" de formulário do Pages
- 4. Conector "API própria / CRM próprio" (Integrações)
- API `/v1/webhooks`
- Retentativas e prazos
- Como responder
- Idempotência
- Segurança e rede
- Formatos antigos (descontinuados)
- Planejado
Webhooks de saída
Este guia descreve o que o Asender envia para o seu servidor por HTTP: o formato de cada chamada, como conferir a assinatura, o que acontece quando o seu endpoint falha e como tornar o seu receptor seguro. Ele descreve o que o código faz hoje; o que ainda não existe aparece como "planejado" (veja a última seção).
Um formato só: Asender Webhook v1. Os quatro emissores do Asender (eventos da conta, CRM, formulários do Páginas e conector de Integrações) enviam os mesmos três cabeçalhos —Asender-Webhook-Id,Asender-EventeAsender-Signature— e um único verificador serve a todos. Implemente o v1 e ignore os cabeçalhos antigos: eles continuam sendo enviados só até 2027-04-20 (veja Formatos antigos).
Visão geral
| # | Emissor | Eventos que envia (Asender-Event) | Onde se cadastra |
|---|---|---|---|
| 1 | Eventos da conta (e-mail, SMS, WhatsApp, push, Mail) | email.queued, email.failed, sms.sent, sms.failed, whatsapp.sent, whatsapp.failed, push.delivered, push.failed, mail.sent, mail.received, e webhook.test no teste | API pública /v1/webhooks, com chave de API de escopo webhooks:manage (como usar) |
| 2 | Destinos de lead do CRM | crm.lead.captured (lead novo) e crm.teste (botão Testar) | painel de Marketing, tela Integrações → Nova integração |
| 3 | Destino "webhook" de formulário (Páginas) | form.submitted (cada submissão aceita) | editor do formulário, em Destinos → Webhook |
| 4 | Conector "API própria / CRM próprio" (Integrações) | connector.delivery (um lead de formulário entregue à sua API) | painel de Admin, tela Integrações |
Pontos comuns aos quatro:
- O método é
POSTcom corpo JSON (o conector do item 4 também aceita os métodosPUT/PATCHe formulárioapplication/x-www-form-urlencoded, veja a seção 4). - Responda
2xxrápido e processe depois. Qualquer outra resposta, ou o estouro do prazo, é falha e gera nova tentativa (a política exata de cada emissor está em Retentativas). - A entrega é "ao menos uma vez". O mesmo evento pode chegar mais de uma vez. Seu receptor precisa ser idempotente: deduplique por
Asender-Webhook-Id. - A assinatura é calculada sobre os bytes exatos do corpo. Leia o corpo cru, confira a assinatura e só então interprete o JSON.
- Sem segredo, sem assinatura. Os emissores 2, 3 e 4 têm segredo opcional: sem ele a entrega sai sem
Asender-Signature(configure sempre um segredo). O emissor 1 não entrega a endpoint sem segredo — o segredo é sempre gerado pelo Asender.
O formato v1
Cabeçalhos
| Cabeçalho | Valor |
|---|---|
Content-Type | application/json (o conector 4 também pode enviar application/x-www-form-urlencoded) |
Asender-Webhook-Id | id da entrega, o mesmo em todas as tentativas da mesma entrega. É opaco: trate como texto, sem interpretar o formato |
Asender-Event | o tipo do evento (veja a tabela da visão geral) |
Asender-Signature | t=<unix>,v1=<hex> — presente só quando há segredo |
Sem prefixo X-, de acordo com a RFC 6648.
Como a assinatura é calculada
conteudo_assinado = "<t>" + "." + <corpo cru, byte a byte>
v1 = hex minúsculo( HMAC-SHA256( segredo, conteudo_assinado ) )
Asender-Signature = "t=<t>,v1=<v1>"
té o horário da tentativa em segundos Unix (UTC), em decimal. Cada tentativa é assinada de novo com otda própria tentativa; só oAsender-Webhook-Idé estável.- O segredo entra como a string que você guardou (
whsec_…no emissor 1), sem decodificar nada. - Tolerância de 300 segundos (5 minutos): recuse a entrega se
|agora − t| > 300. Como cada tentativa leva umtnovo, a janela não atrapalha as retentativas. - Vários
v1=na rotação. O valor pode trazer mais de umv1=(por exemplot=1792485815,v1=aaa…,v1=bbb…), cada um calculado com um segredo diferente, para que você troque o segredo sem janela de falha. Aceite a entrega se qualquerv1=conferir com o segredo que você tem. Os verificadores abaixo já fazem isso. Hoje os emissores enviam um únicov1=; o endpoint de rotação ainda é planejado. - Compare em tempo constante (nunca com
==) e sobre o corpo cru: reserializar o JSON antes de conferir quebra a assinatura.
Exemplo (segredo whsec_exemplo, corpo {"ok":true}):
Asender-Webhook-Id: whd_8f3a2c1d4e5b6a7988776655aabbccdd
Asender-Event: email.failed
Asender-Signature: t=1792485815,v1=<hex de 64 caracteres>
Regras do receptor
- Leia o corpo como bytes crus, antes de parsear o JSON.
- Confira a assinatura (tempo constante) e a tolerância. Se falhar, responda
401sem processar nada. - Deduplique por
Asender-Webhook-Id, na mesma transação em que grava o efeito. - Responda
2xxsó depois de gravar o evento; o trabalho pesado vem depois.
Verificadores (v1)
Um verificador por linguagem. Cada um recebe o segredo, os cabeçalhos com nomes em minúsculas e o corpo cru, e devolve verdadeiro só se há um t dentro da tolerância e algum v1 confere. Eles foram executados contra assinaturas geradas pelo mesmo algoritmo dos emissores, com corpo adulterado, segredo errado, t velho, t futuro, cabeçalho ausente e vários v1= na rotação.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCIA_S = 300; // 5 minutos
// h: cabeçalhos com nomes em minúsculas (req.headers do Node já vem assim).
// corpo: Buffer com os bytes CRUS do request. agora: segundos Unix.
export function verificarAssinaturaV1(segredo, h, corpo, agora = Date.now() / 1000) {
let t = null;
const v1s = [];
for (const parte of (h["asender-signature"] ?? "").split(",")) {
const i = parte.indexOf("=");
if (i < 0) continue;
const k = parte.slice(0, i).trim();
const v = parte.slice(i + 1).trim();
if (k === "t") t = v;
else if (k === "v1") v1s.push(v);
}
if (t === null || !/^\d+$/.test(t) || v1s.length === 0) return false;
if (Math.abs(agora - Number(t)) > TOLERANCIA_S) return false;
const esperado = createHmac("sha256", segredo).update(`${t}.`).update(corpo).digest();
return v1s.some((v) => {
const recebido = Buffer.from(v, "hex");
return recebido.length === esperado.length && timingSafeEqual(esperado, recebido);
});
}
Uso com o servidor HTTP nativo (o corpo cru é o Buffer acumulado):
import http from "node:http";
import { verificarAssinaturaV1 } from "./verificacao.mjs";
http.createServer(async (req, res) => {
const pedacos = [];
for await (const p of req) pedacos.push(p);
const corpo = Buffer.concat(pedacos); // bytes crus
if (!verificarAssinaturaV1(process.env.WEBHOOK_SEGREDO, req.headers, corpo)) {
res.writeHead(401).end();
return;
}
const id = req.headers["asender-webhook-id"]; // chave de deduplicação
const evento = JSON.parse(corpo.toString("utf8")); // só depois de conferir
// grave (id, evento) com restrição de unicidade em id e responda rápido
res.writeHead(204).end();
}).listen(3000);
Python
import hashlib
import hmac
import re
import time
TOLERANCIA_S = 300 # 5 minutos
def verificar_assinatura_v1(segredo: str, h: dict, corpo: bytes, agora=None) -> bool:
"""h: cabeçalhos com nomes em minúsculas. corpo: bytes CRUS, antes de qualquer json.loads."""
agora = time.time() if agora is None else agora
t, v1s = None, []
for parte in h.get("asender-signature", "").split(","):
k, sep, v = parte.strip().partition("=")
if not sep:
continue
if k == "t":
t = v
elif k == "v1":
v1s.append(v)
if t is None or not re.fullmatch(r"[0-9]+", t) or not v1s:
return False
if abs(agora - int(t)) > TOLERANCIA_S:
return False
esperado = hmac.new(segredo.encode(), t.encode() + b"." + corpo, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(esperado.encode(), v.lower().encode()) for v in v1s)
Uso com o servidor da biblioteca padrão:
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from verificacao import verificar_assinatura_v1
class Receptor(BaseHTTPRequestHandler):
def do_POST(self):
corpo = self.rfile.read(int(self.headers.get("Content-Length", 0))) # bytes crus
h = {k.lower(): v for k, v in self.headers.items()}
if not verificar_assinatura_v1(os.environ["WEBHOOK_SEGREDO"], h, corpo):
self.send_response(401)
self.end_headers()
return
webhook_id = h.get("asender-webhook-id") # chave de deduplicação
# json.loads(corpo) só depois de conferir; grave (webhook_id, evento) e responda rápido
self.send_response(204)
self.end_headers()
HTTPServer(("", 3000), Receptor).serve_forever()
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
const toleranciaS = 300 // 5 minutos
// VerificarAssinaturaV1 confere o Asender-Signature (t=<unix>,v1=<hex>[,v1=<hex>]).
// h: cabeçalhos com nomes em minúsculas. corpo: os bytes CRUS do request.
func VerificarAssinaturaV1(segredo string, h map[string]string, corpo []byte, agora time.Time) bool {
var ts string
var v1s []string
for _, parte := range strings.Split(h["asender-signature"], ",") {
k, v, ok := strings.Cut(strings.TrimSpace(parte), "=")
if !ok {
continue
}
switch k {
case "t":
ts = v
case "v1":
v1s = append(v1s, v)
}
}
t, err := strconv.ParseInt(ts, 10, 64)
if err != nil || len(v1s) == 0 {
return false
}
if d := agora.Unix() - t; d > toleranciaS || d < -toleranciaS {
return false
}
m := hmac.New(sha256.New, []byte(segredo))
m.Write([]byte(ts + "."))
m.Write(corpo)
esperado := m.Sum(nil)
for _, v := range v1s {
if recebido, err := hex.DecodeString(v); err == nil && hmac.Equal(esperado, recebido) {
return true
}
}
return false
}
Uso com net/http (lembre-se de passar os cabeçalhos com nomes em minúsculas):
func receptor(w http.ResponseWriter, r *http.Request) {
corpo, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) // bytes crus
if err != nil {
http.Error(w, "corpo inválido", http.StatusBadRequest)
return
}
h := map[string]string{}
for k := range r.Header {
h[strings.ToLower(k)] = r.Header.Get(k)
}
if !VerificarAssinaturaV1(os.Getenv("WEBHOOK_SEGREDO"), h, corpo, time.Now()) {
http.Error(w, "assinatura inválida", http.StatusUnauthorized)
return
}
id := h["asender-webhook-id"] // chave de deduplicação
_ = id // grave (id, evento) com unicidade em id; decodifique o JSON só agora
w.WriteHeader(http.StatusNoContent)
}
PHP
Requer PHP 8.0 ou superior.
<?php
const TOLERANCIA_S = 300; // 5 minutos
// $h: cabeçalhos com nomes em minúsculas. $corpo: os bytes CRUS (file_get_contents('php://input')).
function verificar_assinatura_v1(string $segredo, array $h, string $corpo, ?int $agora = null): bool {
$agora ??= time();
$t = null;
$v1s = [];
foreach (explode(',', $h['asender-signature'] ?? '') as $parte) {
[$k, $v] = array_pad(explode('=', trim($parte), 2), 2, null);
if ($v === null) {
continue;
}
if ($k === 't') {
$t = $v;
} elseif ($k === 'v1') {
$v1s[] = $v;
}
}
if ($t === null || !ctype_digit($t) || $v1s === []) {
return false;
}
if (abs($agora - (int) $t) > TOLERANCIA_S) {
return false;
}
$esperado = hash_hmac('sha256', $t . '.' . $corpo, $segredo);
foreach ($v1s as $v) {
if (hash_equals($esperado, strtolower($v))) {
return true;
}
}
return false;
}
Uso (o corpo cru vem de php://input; o PHP expõe os cabeçalhos em $_SERVER):
$corpo = file_get_contents('php://input');
$h = [
'asender-signature' => $_SERVER['HTTP_ASENDER_SIGNATURE'] ?? '',
'asender-webhook-id' => $_SERVER['HTTP_ASENDER_WEBHOOK_ID'] ?? '',
];
if (!verificar_assinatura_v1(getenv('WEBHOOK_SEGREDO'), $h, $corpo)) {
http_response_code(401);
exit;
}
$evento = json_decode($corpo, true); // só depois de conferir
// grave ($h['asender-webhook-id'], $evento) com unicidade no id
http_response_code(204);
1. Eventos da conta
Notificam o resultado do processamento de uma mensagem enviada pela sua conta: e-mail, SMS, WhatsApp, push e a caixa do Asender Mail. É o emissor com cadastro pela API pública, tipos de evento e filtro por tipo.
Cadastro
Pela API pública (/v1/webhooks, chave com o escopo webhooks:manage; veja API /v1/webhooks) ou pelo painel em admin.asender.net → Desenvolvedores → Webhooks (admin ou dono da conta). O segredo (whsec_…) é gerado pelo Asender e mostrado uma única vez, na criação. Endpoint sem segredo não recebe entrega.
Tipos de evento
O corpo é o evento como o serviço de origem o publicou, e o tipo vem em event_type (e em Asender-Event). Hoje os serviços publicam:
| Tipo | Origem | Campos além do envelope |
|---|---|---|
email.queued | e-mail de marketing aceito pelo relay | relay_status, relay_body |
email.failed | e-mail de marketing recusado de forma definitiva | error_code, detail |
sms.sent, sms.failed | SMS | provider_message_id, segments / error_code, detail |
whatsapp.sent, whatsapp.failed | provider_message_id / error_code, detail | |
push.delivered, push.failed | push | provider_message_id / error_code, detail |
mail.sent, mail.received | caixa do Asender Mail | data com from, to, subject (enviado) ou from, subject, mailbox_id (recebido) |
webhook.test | só quando você aciona POST /v1/webhooks/{id}/test | data com test e endpoint_id |
Não existe email.delivered nem email.opened/clicked/bounced neste emissor hoje. Eventos de uso interno (prefixo interno.) nunca são entregues a webhook de cliente. webhook.test não é assinável: ele só chega quando você pede o teste.
Ao cadastrar um endpoint, a lista de eventos (Events, de 1 a 32 itens, sem repetição) aceita padrões: * (tudo), uma família com ponto e asterisco (email.*, sms.*, whatsapp.*, push.*, mail.*) ou o tipo exato (email.failed).
Exemplo de corpo (email.failed)
{
"detail": "mailbox unavailable",
"error_code": "relay_5xx_550",
"event_type": "email.failed",
"message_id": "msg_01JABCDEF",
"tenant_id": "acc_01JABCDEF",
"timestamp": "2026-10-05T14:03:11.123456789Z"
}
Todo evento traz event_type, tenant_id (a sua conta) e timestamp, e quase sempre message_id. Os eventos mail.* trazem o horário sem frações de segundo e põem os detalhes dentro de data. Trate campos desconhecidos como esperados: novos campos podem ser acrescentados sem aviso. Por privacidade, o número de telefone do destinatário de SMS e WhatsApp não vai no evento; use message_id para consultar a mensagem pela API autenticada.
Asender-Webhook-Id
whd_ seguido de 32 caracteres hexadecimais. É derivado do evento e do endpoint, então é único por evento e por endpoint e igual em toda retentativa. Se você rotacionar o segredo do endpoint (apagando e recriando), as entregas que ainda estavam em retentativa passam a levar outro id.
Retentativa
Veja a tabela: 6 tentativas, prazo de 15 s. Se o seu host acumular falhas seguidas (5), as entregas para ele são adiadas por 60 segundos antes de uma nova sonda — o evento não se perde, só espera.
2. Destinos de lead do CRM
Entregam ao seu sistema cada lead novo capturado pelo CRM (por exemplo, vindo de um formulário). São os "webhooks" da tela Integrações do painel de Marketing e da rota /v1/destinos da API do CRM.
Quando dispara
Uma vez para cada contato novo criado no CRM e para cada destino ativo da conta. Uma pessoa que já existe e só tem os dados atualizados não gera nova entrega. Também dispara quando você aciona o teste do destino.
Onde configurar
No painel de Marketing, Integrações → Nova integração: nome, endereço (URL http ou https) e segredo. Pela API (rotas de sessão do CRM, sem API key):
curl -X POST 'https://api.crm.asender.net/v1/destinos' \
-H 'Authorization: Bearer <token-de-sessão>' \
-H 'X-Asender-Tenant: <conta>' \
-H 'Content-Type: application/json' \
-d '{ "nome": "Meu sistema", "url": "https://hooks.exemplo.test/asender", "segredo": "um-segredo-longo-e-aleatorio", "ativo": true }'
O segredo é opcional e escolhido por você. Ele é guardado cifrado e nunca é devolvido (a API só informa tem_segredo). Sem segredo, a entrega sai sem assinatura e a tela marca o destino como "Sem assinatura" — configure sempre um segredo.
v1 neste emissor
| Cabeçalho | Valor |
|---|---|
Asender-Webhook-Id | o número da entrega no CRM (inteiro decimal, como texto); igual em todas as tentativas |
Asender-Event | crm.lead.captured ou crm.teste |
Asender-Signature | t=<unix>,v1=<hex>, só com segredo |
Corpo
{
"dados": {
"contato_id": "con_01JABCDEF",
"criado": true,
"ocorreu_em": "2026-10-05T14:03:11.123456Z"
},
"evento": "crm.lead.captured",
"tentativa": 1
}
| Campo | Significado |
|---|---|
evento | crm.lead.captured para lead real; crm.teste para o botão Testar |
dados.contato_id | o id público do contato no CRM (use-o para consultar o contato) |
dados.criado | sempre true nas entregas de lead real |
dados.ocorreu_em | quando o lead foi capturado (RFC 3339, UTC) |
tentativa | número da tentativa (1 na primeira) |
O corpo é enxuto de propósito: não carrega e-mail, telefone nem outro dado pessoal do lead — só o identificador. Para obter os dados, consulte o contato no CRM. A ordem das chaves não é garantida; confira a assinatura sobre os bytes crus.
Como testar
Use o botão Testar do destino, ou a rota:
curl -X POST 'https://api.crm.asender.net/v1/destinos/<id>/testar' \
-H 'Authorization: Bearer <token-de-sessão>' \
-H 'X-Asender-Tenant: <conta>'
A resposta é 202 com { "enfileiradas": 1, … }: o teste entra na mesma fila das entregas reais, com a mesma assinatura. Ele só vai para o destino que você escolheu e chega com "evento": "crm.teste" e dados contendo teste: true. O resultado (estado, código HTTP, número de tentativas e o último erro) aparece em GET /v1/destinos/<id>/entregas e na própria tela.
Integrações de conversão
A tela Integrações também tem integrações de conversão (hoje, Meta Conversions API). Elas não são webhooks seus: o Asender chama a API da plataforma de anúncios com o token que você informou, e essa chamada não leva Asender-Signature — a plataforma de anúncios autentica pelo token dela.
3. Destino "webhook" de formulário do Pages
Cada formulário do Páginas pode ter destinos de captura (CRM, Marketing, integração e webhook). O destino webhook entrega os campos enviados pelo visitante ao seu servidor.
Quando dispara
A cada submissão aceita do formulário. A submissão e o despacho são gravados juntos e a entrega acontece em segundo plano: um endpoint fora do ar atrasa a notificação, mas não a perde (até o limite de tentativas), e não atrasa a resposta ao visitante.
Onde configurar
No editor do formulário, em Destinos → Webhook: URL e, opcionalmente, o segredo e o mapa de campos (qual campo do formulário vira qual chave do JSON).
v1 neste emissor
| Cabeçalho | Valor |
|---|---|
Asender-Webhook-Id | id único desta entrega (whk_…, opaco), único por submissão e por destino; igual em todas as tentativas |
Asender-Event | form.submitted |
Asender-Signature | t=<unix>,v1=<hex>, só com segredo |
Corpo
O corpo é um objeto plano com os campos do formulário (valores em texto), usando como chave o alvo do mapa de campos (email, telefone, nome) ou, se o campo não tiver alvo, o próprio nome do campo:
{
"email": "ana@test.asender.com.br",
"nome": "Ana Teste",
"orcamento": "5000"
}
Não há envelope, tipo de evento nem identificador da submissão no corpo — o identificador está em Asender-Webhook-Id e o tipo em Asender-Event. Os campos são dados pessoais do visitante: trate-os conforme a LGPD.
Redirecionamento
Este emissor não segue redirecionamentos: um 3xx é falha de entrega ("redirecionamento não seguido") e é retentado como qualquer outra; cadastre a URL final e responda 2xx direto nela. Destino que aponta para a rede interna ou tem URL inválida é encerrado sem retentar.
4. Conector "API própria / CRM próprio" (Integrações)
O app API própria / CRM próprio da plataforma de Integrações envia o lead de um formulário do Páginas para qualquer API HTTPS que você aponte, com método, autenticação, corpo e critério de sucesso configuráveis. É o mais flexível dos quatro: além do v1 ele leva Idempotency-Key e Traceparent.
Quando dispara
Para cada lead capturado por um formulário do Páginas que tenha este app como destino. Hoje o app só aceita leads de formulários do Páginas.
Onde configurar
No painel de Admin, tela Integrações, instale o app API própria / CRM próprio e vincule-o como destino (tipo "integração") do formulário. Campos:
| Campo | Valores |
|---|---|
| URL | https obrigatório, com nome de host com domínio (IP literal não), sem usuário/senha; porta 443, 8443 ou de 1024 a 65535 |
| Método | POST (padrão), PUT ou PATCH |
| Formato | json (padrão) ou form |
| Corpo (modelo) | modelo com {{variáveis}} (veja abaixo) |
| Cabeçalhos fixos | até 20, não secretos; não pode fixar Authorization, Cookie, Idempotency-Key nem X-Asender-* |
| Autenticação | nenhuma, api_key (cabeçalho — padrão X-API-Key — ou query api_key), bearer, basic ou oauth2_client_credentials |
| Assinar o corpo | liga o HMAC-SHA256 (o segredo precisa de pelo menos 16 caracteres; se você deixar vazio, é gerado) |
| Critério de sucesso | opcional: um caminho JSON da resposta que precisa ser igual a um valor |
O painel mostra uma prévia da requisição com os segredos mascarados e permite testar a conexão antes de ativar.
Cabeçalhos que o Asender sempre envia
| Cabeçalho | Valor |
|---|---|
Content-Type | application/json; charset=utf-8 (ou application/x-www-form-urlencoded) |
Asender-Webhook-Id | id da entrega (ent_…); igual em todas as tentativas |
Asender-Event | connector.delivery |
Asender-Signature | t=<unix>,v1=<hex>, só se "Assinar o corpo" estiver ligado |
Idempotency-Key | o mesmo id da entrega; repasse-o à sua API se ela aceita chave de idempotência |
Traceparent | contexto de rastreio W3C, estável por entrega (o trace) e específico por tentativa (o span) |
Authorization / cabeçalho da chave | conforme o modo de autenticação escolhido |
Corpo
O corpo é o modelo que você escreveu, renderizado com o lead. As variáveis disponíveis são contato.*, campos["…"], consentimento.*, origem.*, formulario.*, pagina.*, submissao.*, qualidade.*, entrega.*, instalacao.* e agora, com filtros como |e164, |data_iso, |digitos e |cortar:10. Exemplo de modelo e do corpo resultante para o lead de exemplo do catálogo:
{
"nome": "{{contato.nome}}",
"email": "{{contato.email}}",
"telefone": "{{contato.telefone|e164}}",
"origem": "{{origem.utm_source}}",
"formulario": "{{formulario.nome}}",
"recebido_em": "{{submissao.recebida_em}}"
}
{"nome":"Ana Teste","email":"ana@test.asender.com.br","telefone":"+5511999990000","origem":"google","formulario":"Fale conosco","recebido_em":"2026-10-05T14:03:11Z"}
O modelo é validado ao salvar (variável desconhecida ou filtro inválido são recusados) e a saída tem teto de tamanho.
Como o Asender classifica a resposta
Prazo de 10 segundos por tentativa (e de 64 KB para a resposta lida); redirecionamentos não são seguidos (um 3xx encerra a entrega com erro — cadastre a URL final).
| Resposta | O que acontece |
|---|---|
2xx (e critério de sucesso, se houver, atendido) | entregue |
2xx com o critério de sucesso não atendido | erro definitivo, sem retentar |
408, 425, 429, 5xx (exceto 501 e 505) e falha de rede/timeout | retenta; se vier Retry-After (segundos ou data), ele é respeitado até 1 h |
401 e 403 | a entrega pausa e a instalação passa a pedir credencial nova; não gasta tentativa |
qualquer outro 4xx | erro definitivo, sem retentar |
API /v1/webhooks
Gerencia os endpoints do emissor 1 (eventos da conta). Exige uma chave de API com o escopo webhooks:manage (Authorization: Bearer <chave>); app OAuth de terceiros nunca recebe esse escopo. A conta vem da chave, nunca da requisição. Os campos do JSON usam PascalCase. Referência completa: API pública e chaves de API.
| Rota | O que faz |
|---|---|
GET /v1/webhooks | lista os endpoints da conta (sem o segredo) |
POST /v1/webhooks | cadastra um endpoint; devolve o segredo uma única vez |
GET /v1/webhooks/{id} | um endpoint (de outra conta ou inexistente: 404 idêntico) |
PATCH /v1/webhooks/{id} | altera Url, Events e/ou Active (false pausa as entregas); não rotaciona o segredo |
DELETE /v1/webhooks/{id} | apaga o endpoint (204) |
POST /v1/webhooks/{id}/test | envia um evento webhook.test assinado a esse endpoint |
Criar
Url precisa ser https em produção, sem credencial nem fragmento e sem destino interno (loopback, faixa privada, link-local/metadados de nuvem, localhost, .local, .internal); o destino é conferido de novo, no endereço resolvido, a cada entrega. Events aceita *, <família>.* ou tipos exatos do catálogo. Esta rota não guarda estado de idempotência: repetir o POST cria outro endpoint (em dúvida, liste antes).
curl -X POST 'https://api.asender.net/v1/webhooks' \
-H 'Authorization: Bearer <chave-com-webhooks:manage>' \
-H 'Content-Type: application/json' \
-d '{ "Url": "https://hooks.exemplo.com.br/asender", "Events": ["email.*", "sms.failed"], "Active": true }'
Resposta 201 (com Cache-Control: no-store):
{
"Id": "whk_83fcaff4b496c6e6",
"Url": "https://hooks.exemplo.com.br/asender",
"Events": ["email.*", "sms.failed"],
"Active": true,
"Secret": "whsec_0123456789abcdef…",
"RequestId": "…"
}
Guarde o Secret agora: ele nunca mais é exibido. Perdeu? Apague o endpoint e crie outro. O segredo é sempre gerado pelo Asender; não é aceito o seu. Guarde-o só no servidor (cofre ou variável de ambiente).
Alterar, listar e apagar
# pausar
curl -X PATCH 'https://api.asender.net/v1/webhooks/whk_83fcaff4b496c6e6' \
-H 'Authorization: Bearer <chave>' -H 'Content-Type: application/json' \
-d '{ "Active": false }'
# listar
curl 'https://api.asender.net/v1/webhooks' -H 'Authorization: Bearer <chave>'
# apagar
curl -X DELETE 'https://api.asender.net/v1/webhooks/whk_83fcaff4b496c6e6' \
-H 'Authorization: Bearer <chave>'
Testar
curl -X POST 'https://api.asender.net/v1/webhooks/whk_83fcaff4b496c6e6/test' \
-H 'Authorization: Bearer <chave>'
Faz um POST síncrono, sem retentativa, só para o endpoint pedido (o evento webhook.test não vai para os outros endpoints da conta), com os cabeçalhos v1 e os antigos. Responde 200 sempre que o endpoint existe; o resultado da entrega vem no corpo e o corpo da resposta do seu servidor não é devolvido:
{ "WebhookId": "whk_83fcaff4b496c6e6", "EventId": "evt_…", "EventType": "webhook.test",
"Delivered": true, "StatusCode": 204, "Reason": "", "DurationMs": 87 }
EventId é o mesmo valor que chega em Asender-Webhook-Id. Reason é "" (entregue), non_2xx, transport_error, destination_blocked (destino interno) ou non_production_blocked (fora de produção só vão domínios de teste, como *.test.asender.com.br, .test e .example).
Retentativas e prazos
| 1. Eventos da conta | 2. CRM | 3. Formulário | 4. Conector | |
|---|---|---|---|---|
| Prazo por tentativa | 15 s | 10 s | 8 s | 10 s |
| Tentativas | 6 | 8 | 12 | 12 |
| Espera entre tentativas | 1 min, 5 min, 15 min, 1 h, 6 h | 30 s dobrando, até 32 min | 30 s dobrando, até 6 h | 30 s dobrando, até 6 h (±20%); respeita Retry-After até 1 h |
| Duração total aproximada | 7 h 20 min | 1 h | 14 h 30 min | 14 h 30 min |
| O que retenta | tudo que não for 2xx | tudo que não for 2xx | tudo que não for 2xx | 408, 425, 429, 5xx e rede |
| Redirecionamento | não segue (3xx é falha) | não segue | não segue (3xx é falha, retentada) | não segue (erro) |
| Depois da última | descartado | falhou | encerrado com o erro | estado "morta" |
Os tempos são aproximados: as entregas são processadas em ciclos, então a tentativa pode atrasar alguns segundos além do previsto. Esgotadas as tentativas, nenhum emissor reenvia sozinho. Destino que aponta para a rede interna ou tem URL inválida falha na hora, sem retentar. Em todos, cada tentativa é assinada de novo com o t da própria tentativa.
Como responder
- Responda
2xxassim que gravar o evento; o corpo da resposta é ignorado. - Faça o trabalho pesado depois de responder (fila própria). Não deixe a chamada esperando banco ou terceiros: estourar o prazo é falha e dispara retentativa.
- Para pedir que o Asender pare de retentar um evento que você não quer, responda
2xxe descarte. (No conector, um4xxque não seja408,425ou429também encerra a entrega.) - Se você precisa recusar por assinatura inválida, use
401. Lembre que os emissores 1, 2 e 3 retentam isso; só o conector (4) trata401como pausa de credencial.
Idempotência
Deduplique por Asender-Webhook-Id, em qualquer emissor: ele é estável entre as tentativas da mesma entrega. Guarde-o na mesma transação em que grava o efeito, com uma restrição de unicidade, e trate a violação como "já processado" (responda 2xx). O id é opaco: não interprete o formato, e não o compare entre emissores diferentes (os formatos são independentes). A janela de 300 s protege contra replay; o id protege contra duplicata.
Segurança e rede
- Valide a assinatura. Não há lista de endereços IP de saída publicada nem estável para os webhooks; não use IP como autenticação. A assinatura é o único controle confiável. Prefira
httpsno seu endpoint. - Segredo só no servidor. Guarde o segredo em cofre ou variável de ambiente do backend, nunca em código de navegador ou repositório. Os verificadores aceitam vários
v1=para que a troca de segredo seja sem janela de falha. - O Asender recusa destinos internos. URLs que apontam para
localhost, endereços privados (10.x,192.168.x,172.16–31.x) ou link-local (inclui169.254.169.254) são recusadas, em qualquer ambiente — conferindo o endereço que o nome resolve no momento de conectar, e não só o nome (o conjunto exato de faixas varia um pouco entre os emissores). Para desenvolver localmente, exponha o seu receptor por um túnel público (HTTPS). - Fora de produção nada sai para destino real. Nos ambientes de teste e homologação, os emissores 1, 2 e 3 só entregam em domínios de teste (
.test,.invalid,.example,example.com,test.asender.com.br); para qualquer outro destino a entrega não é feita, de propósito, para que dados de teste nunca cheguem a um sistema de verdade. O conector (4) não envia em homologação: registra a requisição e marca a entrega como simulada. Use um domínio de teste ao testar. - Limite o tamanho do corpo que o seu receptor aceita e leia o corpo uma só vez.
- Registre o
Asender-Webhook-Id(e oTraceparent, no conector) nos seus logs: facilita achar uma entrega específica quando abrir um chamado.
Formatos antigos (descontinuados)
Sunset: 2027-04-20. Até essa data cada emissor continua enviando os cabeçalhos abaixo, inalterados, além do v1. Depois dela eles saem (com decisão registrada à parte). Migre para o v1 antes; não escreva código novo sobre estes.
| Emissor | Cabeçalhos antigos | Assinatura antiga |
|---|---|---|
| 1. Eventos da conta | X-Asender-Event-Type (tipo do evento), X-Asender-Timestamp (UTC, RFC 3339 com nanossegundos, ex. 2026-10-05T14:03:11.123456789Z), X-Asender-Signature | sha256= + hex de HMAC-SHA256(segredo, timestamp + "." + corpo), com o timestamp exatamente como veio no cabeçalho (texto RFC 3339) |
| 2. CRM | X-Asender-Timestamp (segundos Unix), X-Asender-Signature | v1= + hex de HMAC-SHA256(segredo, timestamp + "." + corpo); ausente sem segredo |
| 3. Formulário | X-Asender-Webhook-Id (= Asender-Webhook-Id), X-Asender-Timestamp (segundos Unix, sempre enviado), X-Asender-Signature-V2, X-Asender-Signature | V2: v1= + hex de HMAC(segredo, timestamp + "." + corpo). Legado: sha256= + hex de HMAC(segredo, corpo), sem timestamp (não protege contra replay). Ambos ausentes sem segredo |
| 4. Conector | X-Asender-Entrega-Id (= Asender-Webhook-Id), X-Asender-Tentativa (a partir de 1), X-Asender-Assinatura | t=<unix>,v1=<hex> com HMAC(segredo, t + "." + corpo), idêntico ao Asender-Signature; só com "Assinar o corpo". Idempotency-Key e Traceparent não são do formato antigo e não são cobertos por este sunset |
Observações para quem ainda usa os antigos:
- A janela de tolerância recomendada é a mesma (300 s) onde há timestamp; no emissor 1 o
X-Asender-Timestampé texto RFC 3339, não segundos Unix. - No emissor 2 a deduplicação antiga era por
dados.contato_id; no 1, pormessage_id+event_type. Passe a usarAsender-Webhook-Id. - A assinatura antiga do emissor 3 (
sha256=só sobre o corpo) permite que uma entrega capturada seja reenviada para sempre e continue válida. Migre para o v1. - Os verificadores antigos (um por emissor) saíram deste guia: a fórmula de cada um está na tabela acima e o caminho recomendado é migrar para o v1, cuja verificação é a mesma nos quatro.
Planejado
Nada desta seção existe hoje. Está descrito para você planejar a integração; datas e formatos podem mudar.
- Rotação de segredo com endpoint próprio, enviando dois
v1=durante a transição. Hoje, para trocar o segredo de um endpoint da conta, crie outro endpoint e apague o antigo. - Novos eventos no emissor 1, ainda não entregues por nenhum emissor:
| Evento planejado | Quando |
|---|---|
conversion.sent | uma conversão é enviada a uma plataforma de anúncio |
email.delivered | o provedor do destinatário confirma a entrega de um e-mail de marketing |
email.opened | o destinatário abre um e-mail de marketing |
email.clicked | o destinatário clica num link de um e-mail de marketing |
email.bounced | um e-mail de marketing volta (bounce) |
campaign.finished | uma campanha termina o envio |
Já existem e não são planejados: o lead novo (crm.lead.captured, emissor 2) e a submissão de formulário (form.submitted, emissor 3).