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

  1. REST-ish, pragmatico - nao precisa ser RESTfully puro, mas previsivel
  2. JSON-first - request e response sempre JSON
  3. SES-compatible shape - maioria das respostas segue o formato AWS SES (ja era assim no relay)
  4. Idempotencia - endpoints POST de envio aceitam IdempotencyKey
  5. Versionada - /v1/ no Core; no relay, versionamento via action
  6. Self-documenting - cada resposta inclui RequestId pra 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

PrefixoUso
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

CodeUso
200Sucesso em GET/PUT/DELETE
201Recurso criado (POST)
204Sucesso sem body (OPTIONS, DELETE simples)
400Validacao falhou
401API key ausente/invalida
402Limite de plano excedido (Core)
403Sem permissao / tenant suspenso
404Recurso nao encontrado
405Metodo HTTP errado
409Conflito (ex: email ja existe na lista)
422Validacao semantica (dominio nao verificado, etc)
429Rate limit excedido
500Erro interno
503Servico 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:

Codigos padronizados

Auth & Access:

Validation:

Resources:

Rate limits:

Service:

Specifics:


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:


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

  1. Global per IP: 1000 req/min em auth endpoints (brute force protection)
  2. Per API key: configurado por plano
  3. Per tenant send limit: hourly/daily de envios
  4. 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


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:


12. Seguranca


13. SDK Philosophy

SDKs sao thin wrappers. Devem:

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;