Convenções da API
Autenticação, formato de requisição e resposta, erros, paginação, idempotência, webhooks e limites.
Convenções da API
O contrato comum das APIs públicas da Asender: autenticação, formato de requisição e resposta, erros, paginação, idempotência, limites e versionamento. Cada ferramenta tem a API dela, num endereço próprio:
| API | endereço |
|---|---|
| Plataforma (envio e leitura de dados por API key) | https://api.asender.net |
https://api.mail.asender.net | |
| CRM | https://api.crm.asender.net |
| Páginas | https://api.pages.asender.net |
| Identidade (OIDC / OAuth 2.1) | https://auth.asender.net |
As APIs não falam todas o mesmo dialeto. O que é igual em todas (HTTPS, JSON, Authorization: Bearer, cursor em vez de offset) está abaixo. O que difere (caixa das chaves, envelope de erro, forma da paginação) está dito por superfície — não reaproveite o parser de uma API em outra. Para escolher o meio de integração, comece pelo mapa de integrações.
1. Princípios
- JSON em tudo. Corpo de requisição e de resposta são JSON (
Content-Type: application/json). - Contrato estrito. Campo que a API não conhece é recusado com
400, não ignorado em silêncio. - A conta vem da credencial. O
tenanté sempre resolvido da API key ou do token, nunca de um campo do corpo. - Cursor, nunca offset. Listagens longas paginam por cursor opaco.
- Escrita idempotente onde há efeito. Envio aceita
Idempotency-Key. - Versão no caminho. Todas as rotas públicas começam em
/v1. - Rastreável. Toda resposta traz
X-Request-Id; cite-o ao abrir suporte.
2. Autenticação
Cabeçalho
Authorization: Bearer <credencial>
É a única forma aceita. ?api_key= e X-Api-Key não existem (respondem 401).
Credenciais
| Credencial | Formato | Para quê |
|---|---|---|
| API key de servidor | sk_live_ + 48 hex (produção), sk_test_ + 48 hex (teste) | Integração de máquina, de servidor para servidor. Cada chave tem escopos (email:send, leads:read…); rota fora do escopo responde 403. |
| Token de aplicativo OAuth | JWT (três segmentos) emitido por auth.asender.net | App de terceiro que lê dados da conta com o consentimento do usuário (somente leitura). Ver Entrar com Asender. |
| Chave pública de Web Push | pk_push_… | Só para registrar a inscrição de Web Push do navegador do visitante, em POST /v1/push/site/devices. Não é segredo e não vale em nenhuma outra rota. |
| Sessão OIDC | Bearer do usuário logado | O painel. Não é para integração de máquina. |
Não existe "chave publicável" genérica de leitura: a API key de servidor nunca vai para o navegador. Para o que roda no site do visitante, use as chaves e o embed próprios — ver No navegador.
3. Formato de requisição
Cabeçalhos em POST/PUT/PATCH
Content-Type: application/json
Caixa dos campos (muda por API)
| API | campos |
|---|---|
Plataforma — envio (/v1/emails/*, /v1/sms/send, /v1/push/send…) | PascalCase, no estilo do Amazon SES (Source, Destination.ToAddresses, Message.Subject.Data) |
Plataforma — leitura de dados (/v1/leads, /v1/campaigns, /v1/analytics/*…) | snake_case (next_cursor, updated_after) |
minúsculas (from, to, subject, text, html) | |
| CRM e Páginas | snake_case / português (contatos, proximo, paginas) |
Exemplo (Plataforma): POST https://api.asender.net/v1/emails/send
{
"Source": "noreply@example.com",
"Destination": { "ToAddresses": ["user@example.com"] },
"Message": {
"Subject": { "Data": "Olá", "Charset": "UTF-8" },
"Body": { "Text": { "Data": "Oi!" } }
}
}
Idempotência
Os POST de envio da Plataforma (/v1/emails/send, /v1/emails/batch, /v1/sms/send, /v1/push/send) aceitam o cabeçalho (tem precedência):
Idempotency-Key: uuid-ou-qualquer-texto # 1 a 128 caracteres de [A-Za-z0-9._:-]
ou o campo IdempotencyKey no corpo.
- Mesma chave e mesmo corpo →
202com oMessageIdda mensagem original e"ReusedIdempotency": true. Nada é enfileirado nem cobrado de novo. - Mesma chave e corpo diferente →
409comCode: "IdempotencyConflict". O corpo novo não é enviado. - Chave malformada →
422ValidationError. Nunca é ignorada em silêncio. - Lote (
/v1/emails/batch): a chave do cabeçalho é da requisição e vira uma chave por item (<chave>:<índice>); repetir o lote deduplica item a item.
Nas outras APIs a idempotência é própria: a Mail API devolve 200 com "idempotent_replay": true; o envio de lead em Páginas aceita Idempotency-Key (ou id_externo) e devolve Idempotent-Replayed: true. Está no guia de cada uma.
4. Formato de resposta
Sucesso na Plataforma (envio)
{
"MessageId": "msg_xxx",
"Status": "queued",
"RequestId": "req_xxx"
}
O envio é assíncrono: 202 Accepted significa "aceito e enfileirado", não "entregue".
Códigos HTTP
| Código | Uso |
|---|---|
| 200 | Sucesso em leitura e atualização |
| 201 | Recurso criado |
| 202 | Envio aceito (enfileirado) |
| 400 | Validação falhou (corpo malformado, campo desconhecido, parâmetro inválido) |
| 401 | Credencial ausente ou inválida |
| 403 | Sem permissão, ou chave sem o escopo da rota |
| 404 | Recurso não encontrado (inclui recurso de outra conta) |
| 405 | Método HTTP errado |
| 409 | Conflito (por exemplo IdempotencyConflict) |
| 422 | Regra de negócio (remetente de domínio não verificado, Idempotency-Key malformada) |
| 429 | Limite de taxa ou de cota excedido |
| 5xx | Falha do serviço ou de uma dependência (503 pode trazer Retry-After) |
Paginação
Listagens longas paginam por cursor opaco — passe de volta o valor que recebeu, sem decodificar nem montar. Offset não existe. A forma varia por superfície:
| Superfície | pedido | resposta |
|---|---|---|
Plataforma — dados (/v1/leads, /v1/forms/{id}/submissions, /v1/conversions, /v1/campaigns, /v1/messaging/*, /v1/conversations…) | ?limit= (1–200, padrão 50), ?cursor=, ?updated_after=, ?created_after= (RFC 3339) | { "data": [...], "next_cursor": "…" | null } — null quando acabou |
Plataforma — histórico de e-mail (GET /v1/emails) | ?limit=, ?cursor=, ?status=, ?q= | { "Messages": [...], "NextCursor": "…", "HasMore": true, "RequestId": "…" } |
Plataforma — log de requisições (GET /v1/logs) | ?limit= (1–100), ?after=, ?before= | has_more e next em snake_case (ver o guia da Plataforma) |
| E-mail (mensagens de uma caixa) | ?limit=, ?cursor= | { "messages": [...], "next_cursor": "…" } |
| CRM (contatos, timeline) | ?limite=, ?cursor= | { "contatos": [...], "proximo": "…" } — vazio quando acabou |
| Páginas | ?limite= em algumas coleções | lista sob chave nomeada ({"paginas":[…]}); analytics por janela ?de=&ate= — sem cursor |
Para sincronizar de forma incremental, prefira updated_after (nas rotas de dados da Plataforma) a reler tudo.
GET /v1/leads?limit=100&cursor=eyJ…
{ "data": [ { "id": "ctt_…", "email": "…" } ], "next_cursor": "eyJ…" }
Cabeçalhos de resposta
Presentes na Plataforma (api.asender.net):
X-Request-Id: req_xxx
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Cache-Control: no-store
Nas rotas sujeitas a limite de taxa (ver Limites):
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1681234567 # instante (epoch, segundos) em que a janela reabre
Retry-After: 1 # só em 429 (e em alguns 503): segundos para tentar de novo
5. Erros
Dois envelopes de erro
Cada superfície tem o seu envelope. Leia o Content-Type e o código HTTP primeiro e escolha o parser pela API que respondeu:
| Superfície | envelope |
|---|---|
Plataforma — envio e histórico (/v1/emails*, /v1/sms/send, /v1/push/*…) | { "Error": { "Type", "Code", "Message", "Details" }, "RequestId" } |
Plataforma — leitura de dados (/v1/leads, /v1/campaigns, /v1/analytics/*, /v1/me…) | { "error": { "type", "message" } } |
E-mail (api.mail.asender.net) | { "error": "<Código>", "message": "<texto>" } |
CRM (api.crm.asender.net) | { "erro": "<mensagem>" } |
Páginas — autoria (api.pages.asender.net) | { "erro": "<mensagem>" } |
| Páginas — coleta pública e runtime | { "error": { "message": "<texto>" } } |
Identidade — /token e /userinfo | { "error": "<código OAuth>", "error_description": "<texto>" } (RFC 6749); no /authorize o erro volta por redirecionamento, em ?error= |
Os envelopes coexistem de propósito: unificar seria uma mudança incompatível. Um cliente que fala com mais de uma API precisa de um parser para cada. Na Plataforma, 404 e 405 em /v1/* também usam o envelope SES-like (Code NotFound / MethodNotAllowed), com uma dica quando o caminho é de outra API.
Estrutura do envelope SES-like (Plataforma — envio)
{
"Error": {
"Type": "Sender",
"Code": "ValidationError",
"Message": "Messages must contain at least one entry.",
"Details": { "Destination.ToAddresses": "must contain at least one address" }
},
"RequestId": "req_xxx"
}
Type:Sender(4xx, o cliente errou) ouReceiver(5xx, o serviço falhou).Code: classe estável (ValidationError,AuthorizationError,InsufficientScope,NotFound,IdempotencyConflict,Throttling,InternalError…). Novos códigos podem ser acrescentados; o significado de um existente não muda.Message: texto para humanos.Details: opcional, contexto legível por máquina (em validação, o campo e o motivo).
Os códigos de cada API estão na página dela; aqui só o que é comum.
Quem repete e quem não repete
Repita (com espera) em 429 (respeite Retry-After) e em 502/503. Não repita 400, 401, 403, 404 nem 422: são determinísticos, repetir só repete o erro.
6. Webhooks
A Asender emite webhooks (eventos de entrega da conta, leads do CRM, submissões de formulário e entregas do conector de Integrações) e recebe webhooks de provedores (por exemplo, o status de SMS). Todo webhook que a Asender emite usa o formato único Asender Webhook v1 (ADR-0133): os cabeçalhos Asender-Webhook-Id (estável entre tentativas, a chave de deduplicação), Asender-Event e Asender-Signature: t=<unix>,v1=<hex>, onde v1 é o HMAC-SHA256 do segredo sobre "<t>.<corpo cru>", com tolerância de 300 s e possibilidade de vários v1= durante a rotação do segredo. Os cabeçalhos de assinatura anteriores de cada emissor são descontinuados e saem em 2027-04-20 (Sunset). Os endpoints de eventos da conta são gerenciados pela API pública em /v1/webhooks (escopo webhooks:manage, só chave). O formato, um verificador por linguagem (Node, Python, Go, PHP), as tentativas de cada emissor e a migração estão no guia de webhooks.
Regras que valem para qualquer webhook que você receber:
- verifique a assinatura sobre o corpo cru, em tempo constante, antes de confiar no conteúdo;
- responda rápido com
2xxe processe depois; - trate a entrega como pelo menos uma vez: deduplique pelo id do evento.
7. CORS e navegador
A API de servidor (sk_…) é de servidor para servidor; não a chame do navegador (isso exigiria expor a chave). O que roda no navegador do visitante usa chaves e rotas próprias, com origem autorizada: o registro de Web Push (POST /v1/push/site/devices com pk_push_…) e a coleta de formulários e analytics de Páginas. Veja No navegador.
8. Limites
Limite de taxa (Plataforma)
- Por credencial: cada API key tem o seu balde; cada par app OAuth × conta também (um app barulhento numa conta não consome a cota do mesmo app em outra). O padrão é de 100 requisições por segundo, ajustável pela Asender.
- Rotas de login e de recuperação têm teto próprio por IP (proteção contra força bruta).
- Estourou:
429,Code: "Throttling", comRetry-Aftere osX-RateLimit-*. O limitador é controle de abuso; se ele estiver indisponível, a API segue atendendo.
Cota do plano (E-mail)
A Mail API barra o envio ao atingir o teto do plano com 429 QuotaExceeded e os cabeçalhos X-Quota-* (qual janela barrou, quando reabre, Retry-After).
Outros tetos
Cada API declara os seus (por exemplo, Páginas limita o envio de lead por chave por minuto e o tamanho do corpo). Estão no guia da API.
9. Versionamento
O número da versão vai no caminho (/v1). Mudança incompatível cria uma nova versão maior; mudança compatível (campo novo opcional, valor novo de enum, rota nova) não muda a versão — escreva o cliente para ignorar o que não conhece, nos campos de resposta. (Nos campos de requisição vale o contrário: o corpo é estrito.)
Quando algo for ser descontinuado, a Asender avisa pelo registro de mudanças do guia da API e dá prazo antes de remover. Um cabeçalho Sunset por resposta ainda não é emitido (planejado).
10. Lote de e-mails
POST https://api.asender.net/v1/emails/batch envia até 500 e-mails independentes numa chamada. Escopo email:send.
{
"Messages": [
{ "Source": "noreply@example.com",
"Destination": { "ToAddresses": ["a@example.com"] },
"Message": { "Subject": { "Data": "Olá A" }, "Body": { "Text": { "Data": "Oi A" } } } },
{ "Source": "noreply@example.com",
"Destination": { "ToAddresses": ["b@example.com"] },
"Message": { "Subject": { "Data": "Olá B" }, "Body": { "Text": { "Data": "Oi B" } } } }
]
}
Cada item tem o mesmo formato de POST /v1/emails/send. Resposta, 202:
{
"Results": [
{ "Index": 0, "Status": "queued", "MessageId": "msg_xxx", "MessageIds": ["msg_xxx"], "ReusedIdempotency": false },
{ "Index": 1, "Status": "rejected", "Error": { "Code": "ValidationError", "Details": { "Destination.ToAddresses": "…" } } }
],
"Count": 2,
"RequestId": "req_xxx"
}
Statuspor item:queued,rejected(a validação recusou) oufailed(não deu para enfileirar). Falha de um item não derruba os outros: o lote devolve202mesmo com falha parcial. InspecioneResults[].Status.Messagesvazio ou com mais de 500 itens →400ValidationError.- Use
Idempotency-Keypara poder repetir o lote com segurança (ver seção 3).
11. Filtros
As listagens aceitam filtros por query string, e cada rota documenta os seus. O que é comum nas rotas de dados da Plataforma: limit, cursor, updated_after, created_after (datas em RFC 3339, por exemplo 2026-10-19T12:00:00Z). Valor inválido é 400 (InvalidParameter), nunca "corrigido" em silêncio.
12. Segurança
- HTTPS obrigatório. Nenhuma credencial trafega em HTTP.
- Guarde as chaves no servidor (variável de ambiente ou cofre). Nunca em bundle de navegador, app móvel, repositório ou log.
- Escopo mínimo. Crie uma chave por integração, só com os escopos que ela usa; revogue a que vazou — chaves de API.
- A credencial identifica a conta: não existe parâmetro para escolher outra.
- Respostas da Plataforma não são cacheáveis (
Cache-Control: no-store).
13. SDKs
Hoje o contrato é a OpenAPI unificada e as páginas de cada API; não há SDK oficial de servidor. Se você escrever um cliente, ele deve:
- ler a chave de variável de ambiente;
- repetir com recuo exponencial em
429/5xx(honrandoRetry-After) e não repetir4xx; - usar
Idempotency-Keyem todo envio; - não decodificar o cursor.
No navegador existe o @asender/pages-sdk, para formulários e leads de Páginas — ver No navegador.