Convenções da API
Autenticação, formato de requisição e resposta, erros, paginação, idempotência, webhooks e limites.
API Design - Convencoes do Ecossistema
Padroes de API consistentes entre o Relay PHP e o Asender Core.
1. Principios
- REST-ish, pragmatico - nao precisa ser RESTfully puro, mas previsivel
- JSON-first - request e response sempre JSON
- SES-compatible shape - maioria das respostas segue o formato AWS SES (ja era assim no relay)
- Idempotencia - endpoints POST de envio aceitam
IdempotencyKey - Versionada -
/v1/no Core; no relay, versionamento via action - Self-documenting - cada resposta inclui
RequestIdpra suporte
2. Autenticacao
Header padrao
Authorization: Bearer <key>
Alternativas (pra webhooks/crons)
?api_key=<key> (query param)
X-Api-Key: <key> (custom header - suportado pelo Core, nao pelo relay)
Formato de keys
| Prefixo | Uso |
|---|---|
sk_master_ | Master key do relay (admin) |
sk_live_ | Tenant key do relay OU API key do Core (prod) |
sk_test_ | API key do Core em modo teste |
pk_live_ | Publishable key (client-side, read-only) |
Todas com 48 chars hex depois do prefixo. Hash SHA-256 armazenado.
3. Request Format
Headers obrigatorios em POST/PUT
Content-Type: application/json; charset=utf-8
Body
JSON com chaves em PascalCase (alinhado com SES).
Excecao: paths internos/admin podem usar snake_case se for mais conveniente. Documentado por endpoint.
Exemplo
{
"Source": "noreply@example.com",
"Destination": {
"ToAddresses": ["user@example.com"]
},
"Message": {
"Subject": {"Data": "Hello", "Charset": "UTF-8"},
"Body": {"Text": {"Data": "Hi there"}}
}
}
Idempotencia
POST de envio aceita:
X-Idempotency-Key: uuid-or-any-string
ou no body:
{"IdempotencyKey": "..."}
Se a mesma key eh reutilizada dentro de 24h, retorna o mesmo MessageId sem re-enfileirar.
4. Response Format
Sucesso
{
"MessageId": "msg_xxx",
"Status": "queued",
"RequestId": "req_xxx"
}
Sempre contem RequestId.
HTTP Status Codes
| Code | Uso |
|---|---|
| 200 | Sucesso em GET/PUT/DELETE |
| 201 | Recurso criado (POST) |
| 204 | Sucesso sem body (OPTIONS, DELETE simples) |
| 400 | Validacao falhou |
| 401 | API key ausente/invalida |
| 402 | Limite de plano excedido (Core) |
| 403 | Sem permissao / tenant suspenso |
| 404 | Recurso nao encontrado |
| 405 | Metodo HTTP errado |
| 409 | Conflito (ex: email ja existe na lista) |
| 422 | Validacao semantica (dominio nao verificado, etc) |
| 429 | Rate limit excedido |
| 500 | Erro interno |
| 503 | Servico indisponivel (SMTP down, etc) |
Paginacao
Listagens usam cursor-based OU offset-based.
Offset (simples, usado no relay):
GET /v1/emails?limit=50&offset=100
{
"Messages": [...],
"Count": 50,
"Total": 342,
"RequestId": "..."
}
Cursor (recomendado no Core pra grandes volumes):
GET /v1/emails?limit=50&cursor=eyJpZCI6MTAwfQ
{
"Messages": [...],
"NextCursor": "eyJpZCI6MTUwfQ",
"HasMore": true,
"RequestId": "..."
}
Cursor eh base64 de um JSON opaco com o estado ({id: N} ou similar).
Headers de Response
Sempre presentes:
X-Request-Id: req_xxx
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Quando relevante:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1681234567
Retry-After: 60 (em 429 ou 503)
Location: /v1/emails/msg_xxx (em 201)
5. Erro Format (SES-like)
Estrutura
{
"Error": {
"Type": "Sender",
"Code": "ValidationError",
"Message": "Destination.ToAddresses must contain at least one address.",
"Details": {
"field": "Destination.ToAddresses",
"constraint": "min_length",
"value": 0
}
},
"RequestId": "req_xxx"
}
Campos:
Type:Sender(4xx - erro do cliente) ouReceiver(5xx - erro do servidor)Code: enum estavel (nunca mude semantica, so adicione novos)Message: descricao humana, localizavelDetails(opcional): contexto adicional maquina-legivel
Codigos padronizados
Auth & Access:
AuthorizationError- key ausente/invalidaPermissionDenied- key valida mas sem permissaoTenantSuspended- tenant suspensoPlanLimitExceeded- limite do plano atingido (Core)
Validation:
ValidationError- body invalidoMissingParameterInvalidParameterInvalidJson
Resources:
NotFound- recurso nao existeConflict- duplicataGone- recurso foi deletadoAlreadyExists
Rate limits:
Throttling- rate limit excedidoQuotaExceeded- quota mensal/diaria
Service:
InternalError- 500ServiceUnavailable- 503SmtpError- problema com SMTPProviderError- erro do provedor externo (SES, Twilio, etc)
Specifics:
DomainNotVerifiedSuppressedAddress- destinatario na lista de supressaoInvalidRecipientNotInstalled- relay nao instalado ainda
6. Webhooks
Signature
Outgoing webhooks assinam com HMAC-SHA256:
X-Asender-Signature: t=1681234567,v1=abc123def...
X-Asender-Event: email.sent
X-Asender-Event-Id: evt_xxx
Para verificar:
signed_payload = t + "." + body
expected = hmac_sha256(webhook_secret, signed_payload)
constant_time_compare(expected, v1)
Evita replay: rejeitar se t for mais antigo que 5min.
Payload
{
"EventId": "evt_xxx",
"EventType": "email.delivered",
"CreatedAt": "2026-04-12T15:30:00Z",
"TenantId": "acc_xxx",
"Data": {
"MessageId": "msg_xxx",
...
}
}
Retry
Se endpoint retorna nao-2xx, retry com backoff:
- 1min, 5min, 15min, 1h, 6h, 24h (6 tentativas)
- Apos 24h, marca delivery como
failed
7. CORS
Default
Bloqueia cross-origin (API eh server-to-server).
Allowlist
Customer pode configurar origins permitidos (pra frontend JS usando publishable keys):
Access-Control-Allow-Origin: https://app.customer.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
Preflight OPTIONS sempre responde 204.
8. Rate Limits
Camadas
- Global per IP: 1000 req/min em auth endpoints (brute force protection)
- Per API key: configurado por plano
- Per tenant send limit: hourly/daily de envios
- Per resource: ex: max 100 templates por tenant
Resposta ao atingir
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1681234612
{
"Error": {
"Type": "Sender",
"Code": "Throttling",
"Message": "Rate limit exceeded. Retry after 45 seconds."
}
}
9. Versionamento
Core
URL path: /v1/, /v2/. Mudancas breaking criam nova major version.
Mudancas non-breaking (campos novos opcionais, enums adicionais) nao bumpam versao.
Relay
Via action name. Ex: action=send continua sendo v1. Se precisar breaking, criar action=v2.send.
Deprecacao
- Aviso no header:
Sunset: Wed, 01 Jan 2027 00:00:00 GMT - Docs marcam como deprecated
- Prazo minimo: 6 meses antes de remover
10. Batch Operations
Convencao pra operacoes em lote:
POST /v1/emails/send-batch
{
"Entries": [
{"Id": "client-entry-1", ...payload},
{"Id": "client-entry-2", ...payload}
]
}
Response:
{
"Entries": [
{"Id": "client-entry-1", "MessageId": "msg_xxx", "Status": "queued"},
{"Id": "client-entry-2", "Error": "..."}
],
"SuccessCount": 1,
"FailCount": 1,
"RequestId": "req_xxx"
}
Sempre retorna 200, mesmo com falhas parciais. Cliente deve inspecionar Entries[].Error.
Limite: max 500 entries por request.
11. Filtros e Queries
Listagens aceitam filtros via query params:
GET /v1/emails?status=sent&from=2026-04-01&to=2026-04-30&tag=campaign:welcome
Formato:
- Igualdade simples:
status=sent - Datas: ISO 8601
from=2026-04-12T10:00:00Z - Multiplos valores:
status=sent,deliveredoustatus[]=sent&status[]=delivered - Busca:
q=welcome(full-text nos campos relevantes) - Sort:
sort=-created_at(prefixo-= desc)
12. Seguranca
- HTTPS obrigatorio em producao (relay + core)
- HSTS header em respostas
- **Sem CORS* default** em endpoints sensitivos
- Content-Security-Policy no dashboard
- Rate limit agressivo em auth
- Audit log de acoes admin
- PII masking em logs (emails, phones aparecem como
u***@e***.com) - Encryption at rest de credenciais
- Keys nunca em logs
13. SDK Philosophy
SDKs sao thin wrappers. Devem:
- Setar auth automaticamente via env var
- Retry em 5xx com backoff exponencial (3 tentativas)
- NAO fazer retry em 4xx
- Expor typed response models
- Honrar
Retry-After - Oferecer sync + async (onde aplicavel)
PHP exemplo:
$asender = new Asender\Client([
'api_key' => getenv('ASENDER_API_KEY'),
'base_url' => 'https://api.asender.io',
]);
$result = $asender->emails()->send([
'Source' => 'noreply@example.com',
'Destination' => ['ToAddresses' => ['user@example.com']],
'Message' => [...],
]);
echo $result->MessageId;