# BytSend API > BytSend é uma API de e-mail transacional (estilo Resend): uma chave de API e > um domínio de envio verificado, nada mais. ## URL base **Serviço hospedado (o caso normal): `https://bytsend.com`.** É a URL de todos os exemplos deste documento. Se a conta foi criada em bytsend.com, é essa a URL — não há nada a configurar. Só quem instalou o BytSend no próprio servidor (self-hosted) usa outra URL: o domínio do próprio deploy, no lugar de `https://bytsend.com` nos exemplos. **A URL base é uma constante no código de quem integra — nunca um campo de tela.** Não crie campo "URL da instância", "endereço do servidor" ou "host": quem usa o serviço hospedado não teria o que digitar ali, e quem é self-hosted já sabe o próprio domínio. ## Integração em 3 passos 1. **Chave de API** — prefixo `by_`, criada no painel (Chaves de API). É o único segredo. 2. **E-mail de remetente** — endereço cujo domínio já está verificado (painel → Domínios). 3. **Teste de envio** — `POST /api/emails` com `{from, to, subject, html}`. Não existe endpoint separado de teste: um envio normal **é** o teste. ## Tela de integração no app que consome a API Exatamente **dois campos de configuração** e **um teste**: 1. **Chave de API** — campo secreto, valor com prefixo `by_`. 2. **E-mail de origem (From)** — endereço de um domínio verificado. 3. **Enviar e-mail de teste** — campo para o e-mail de destino + botão. Envia um e-mail real via `POST /api/emails`. Nada além disso. Sem URL, sem host, sem porta, sem cliente OAuth, sem SDK. ```js const res = await fetch('https://bytsend.com/api/emails', { method: 'POST', headers: { Authorization: 'Bearer ' + apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: fromEmail, to: testEmail, subject: 'Teste', html: templateRealDoApp }) }); const corpo = await res.json(); // 200 {"id":"...","from":"...","to":"...","created_at":"..."} -> sucesso // 400 {"error":{"message":"...","type":"..."}} -> mostre corpo.error.message na tela ``` `error` é um **objeto**, não uma string: mostre `error.message`. Concatenar `error` direto imprime `[object Object]`. ## Regras rígidas para quem for integrar (NÃO viole) - NÃO use o SDK/cliente do Resend, chaves `re_` nem chamadas no formato `emails.send`. Aqui a chave tem prefixo `by_` e o envio é `POST /api/emails`. - NÃO chame `/api/send`, `/api/emails/send`, `/v1/emails` ou similares — não existem. - NÃO use os endpoints de conta (`/api/auth/*`) para enviar e-mail — eles usam JWT de login, não a chave de API. - NÃO faça deploy nem instale este repositório para "integrar" — a API já está no ar. Integrar = uma requisição HTTP com a chave. - NÃO crie campo de URL/host/instância na tela de configuração. ## Autenticação - Chave de API como token Bearer: `Authorization: Bearer by_sua_api_key`. - Chaves têm prefixo `by_`; crie em `POST /api/api-keys` (o valor completo aparece uma única vez) ou no painel. - Endpoints de conta (`/api/auth/*`, `/api/billing/*`) usam um JWT de `POST /api/auth/register` ou `POST /api/auth/login`. - Endpoints de recursos aceitam chave de API ou JWT. - Sem cabeçalho `Authorization`: `401 auth_error`. Chave com prefixo `by_` inexistente ou revogada: `401 api_key_error` (`Invalid API key`). Um token **sem** o prefixo `by_` é tratado como JWT e volta `401 auth_error` (`Invalid token`) — se você ver essa mensagem usando uma chave de API, o valor provavelmente foi cortado ou colado errado. ## POST /api/emails — campos | Campo | Obrigatório | Formato | |---|---|---| | `from` | sim | `"nome@dominio.com"`, `"Nome "` ou `{"email":"...","name":"..."}`. O domínio depois do `@` precisa estar **cadastrado nesta conta**, e o casamento é exato: cadastrar `exemplo.com` **não** autoriza `@mail.exemplo.com`. | | `to` | sim | **Um destinatário**: `"pessoa@exemplo.com"` ou `{"email":"...","name":"..."}`. Para vários, use `/api/emails/batch` (ou `cc`/`bcc`). Uma lista aqui **não é rejeitada, e é pior que isso**: a mensagem sai para todos os endereços, a lista de supressões é ignorada e o `to` some da resposta e do histórico. | | `subject` | sim | Texto do assunto. | | `html` | recomendado | Corpo em HTML. | | `text` | não | Versão em texto puro. | | `reply_to` | não | Endereço de resposta. | | `cc`, `bcc` | não | Endereço ou lista de endereços. | | `headers` | não | Objeto: `{"X-Seu-Cabecalho":"valor"}`. | | `attachments` | não | Lista de `{"filename":"nota.pdf","content":"","encoding":"base64","content_type":"application/pdf"}`. `encoding` já é `base64` por padrão. | | `template_id` | não | Usa um template salvo; **substitui** o `subject`/`html`/`text` enviados. Um id que não exista nesta conta é **ignorado em silêncio**, sem erro: se você contava com o template, o e-mail sai com assunto e corpo vazios e ainda responde `200`. Confira o id em `GET /api/templates`. | | `template_variables` | não | Objeto que preenche os `{{marcadores}}` do template. | O corpo inteiro da requisição é limitado a **10 MB** — anexos em base64 ocupam cerca de 33% a mais que o arquivo original. Acima disso a requisição nem chega ao envio, e a resposta **não** é um `email_error`: vem `500 {"error":{"message":"Internal server error","type":"api_error"}}`. Confira o tamanho antes de enviar. Sucesso: `200` com `{"id","from","to","created_at"}` (sem embrulho). ## O que a API confere antes de enviar (nesta ordem) 1. **Conta ativa** — conta bloqueada recusa o envio. 2. **Supressão** — destinatário na lista de supressões é recusado. 3. **Limite do plano** — total acumulado de registros de e-mail da conta; **não há reset mensal**, e tentativas que falharam no SMTP também contam (o registro é criado antes da entrega e fica como `failed`). 4. **Filtro anti-spam** (abaixo). 5. **Domínio cadastrado** — o domínio do `from` precisa existir nesta conta (painel → Domínios). A API **não** exige que a verificação de DNS tenha terminado: um domínio ainda `pending` é aceito no envio, mas sem SPF/DKIM publicados a entrega tende a morrer no relay. A mensagem de erro diz "not verified", mas o que ela checa é a existência do domínio na conta. ## Filtro anti-spam (leia antes de enviar campanha) O conteúdo (`subject` + `html` + `text`) é pontuado antes do envio. **A partir de 2 pontos o e-mail é recusado** com `400 email_error` e a mensagem `Spam detected: `. A pontuação: - **1 ponto por termo** de uma lista de 33 palavras típicas de spam. A lista inclui termos que aparecem em e-mail legítimo — entre eles `unsubscribe`, `urgent`, `winner`, `congratulations`, `limited time`, `order now`, `free money`, `act now`. Dois deles no mesmo e-mail já bastam para recusar. - **`click here` sozinho já recusa**: o termo está duas vezes na lista e soma 2 pontos de uma vez. - **+2 pontos** se o corpo tiver mais de 5 links `http://`/`https://` — sozinho já recusa. A comparação é por **substring, em minúsculas**, sobre assunto + HTML + texto — o HTML conta junto, então um `href`, um `alt` ou uma classe CSS que contenha um desses termos pontua igual. **Três recusas por spam bloqueiam a conta**: a terceira devolve `Account blocked due to repeated spam` e, a partir daí, todo envio falha com `Account blocked` — só um administrador desbloqueia. Em e-mail de marketing, prefira "cancelar inscrição" ou "gerenciar preferências" a `unsubscribe`, evite "click here" e mantenha poucos links. ## Erros Envelope único: `{ "error": { "message": "...", "type": "..." } }`. Tipos: `validation_error`, `auth_error`, `api_key_error`, `not_found_error`, `conflict_error`, `plan_limit_error`, `rate_limit_error`, `email_error`, `api_error`. **Trate `error.type` como opcional**: os erros de validação e conflito de `/api/suppressions`, `/api/webhooks`, `/api/contacts`, `/api/templates` e `/api/audiences` vêm só com `message`. O status HTTP continua distinguindo o caso. **`POST /api/emails` devolve toda falha de envio como `400` com type `email_error`** — o que muda é a `message`. Mensagens reais e o que fazer: | A `message` começa com | Causa | O que fazer | |---|---|---| | `Domain X is not verified for your account` | O domínio do `from` não está **cadastrado** nesta conta (a mensagem diz "not verified", mas basta o domínio existir na conta) | Painel → Domínios: cadastre o domínio exato do remetente e publique o DNS. É o erro mais comum na primeira integração. | | `Email X is suppressed` | Destinatário na lista de supressões | Se foi engano, remova em `DELETE /api/suppressions/{id}`. | | `Email limit reached for ... plan` | Cota de e-mails da conta esgotada | Troque de plano ou fale com o administrador. | | `Spam detected: ...` | Filtro anti-spam (acima) | Reescreva o conteúdo. | | `Account blocked` | Conta bloqueada | Fale com o administrador. | | `Servidor SMTP não configurado` | A instância não tem SMTP configurado | Só em self-hosted: painel Admin → SMTP. | | `Erro ao enviar via SMTP (...)` | O relay recusou a mensagem | O código entre parênteses vem do servidor SMTP. | Rate limit: **1000 requisições por 15 minutos por IP** em todas as rotas `/api/`; acima disso, `429 rate_limit_error`. As respostas trazem os cabeçalhos padrão `RateLimit-*`. ## Endpoints ### E-mails - `POST /api/emails` — envia um e-mail (campos acima) - `POST /api/emails/batch` — `{emails: [...]}`, cada item no mesmo formato; envia em sequência e devolve `{"data":[{"id","status":"success"} | {"status":"error","error":"..."}]}`, na ordem dos itens. O HTTP é `200` mesmo quando itens falham — **confira item a item**. - `GET /api/emails?limit=20&offset=0` — devolve `{"data":[...],"pagination":{"limit","offset","total"}}`, mais recentes primeiro - `GET /api/emails/{id}` — registro completo de um e-mail (status, last_event, provider, erro) ### Domínios - `POST /api/domains` — cadastra um domínio; retorna os registros DNS (TXT de posse `_bytsend-verify`, SPF, DKIM, DMARC e o MX de `bounces.`) - `GET /api/domains` / `GET /api/domains/{id}` — lista / recupera com o status de cada registro - `GET /api/domains/{id}/verify` — recheca o DNS; o domínio vira `active` assim que **DKIM e SPF** verificam — MX e DMARC podem seguir pendentes - `DELETE /api/domains/{id}` — remove ### Chaves de API - `POST /api/api-keys` — cria `{name}`; o `token` completo é retornado uma única vez - `GET /api/api-keys` — metadados (prefixo, last_used_at, active) - `DELETE /api/api-keys/{id}` — revoga ### Contatos, templates, audiências - `GET|POST /api/contacts`, `DELETE /api/contacts/{id}` — `{email, first_name, last_name}` - `GET|POST /api/templates`, `DELETE /api/templates/{id}` — `{name, subject, html, text}` com marcadores `{{variavel}}` - `GET|POST /api/audiences`, `DELETE /api/audiences/{id}` — `{name}` - `GET /api/audiences/{id}/contacts` — contatos de uma audiência (**ainda sem efeito**: não existe rota que associe um contato a uma audiência, então a lista volta vazia) ### Supressões - `GET|POST /api/suppressions`, `DELETE /api/suppressions/{id}` — `{email, reason}`; envios para endereços suprimidos são recusados ### Webhooks - `POST /api/webhooks` — `{url, events[]}`; a assinatura aceita email.sent, email.delivered, email.bounced, email.complained, email.opened e email.clicked, mas **hoje só `email.sent` é entregue de fato** — os outros cinco nunca disparam. Não construa tratamento de bounce/reclamação contando com eles. - `GET /api/webhooks`, `DELETE /api/webhooks/{id}`, `GET /api/webhooks/{id}/deliveries` - Entregas são assinadas: `BytSend-Signature` = HMAC-SHA256 em hex de `"."`. Compare em tempo constante, dentro de um `try/catch` (comparação em tempo constante lança exceção quando o cabeçalho falta ou tem tamanho diferente). - O segredo da assinatura **não é devolvido por nenhuma rota**: em self-hosted é a env `WEBHOOK_SECRET` (se não for definida, é sorteada a cada reinício e as assinaturas param de bater); no serviço hospedado, peça ao administrador. Enquanto não tiver o segredo, proteja o endpoint por HTTPS e um caminho secreto na URL. ### Analytics e conta - `GET /api/analytics` — totais, contagens por domínio, últimos 7 dias - `POST /api/auth/register` / `POST /api/auth/login` — `{email, password}` → objeto do usuário com o JWT no campo `token`. O login também pode devolver `{requires_two_factor:true, two_factor_token}` (conclua em `POST /api/auth/2fa-verify`) ou `403 email_not_verified`; o register pode devolver `requires_verification:true` com um `verification_token`. Sempre cheque esses casos antes de usar `token`. - `GET /api/auth/me` — conta, uso e limites do plano (JWT) - `GET /api/auth/plans` — catálogo de planos; `GET /api/health` — liveness ## Exemplo completo (envio de teste) ```bash curl -X POST https://bytsend.com/api/emails \ -H "Authorization: Bearer by_sua_api_key" \ -H "Content-Type: application/json" \ -d '{"from":"Seu App ","to":"voce@exemplo.com","subject":"Teste","html":"Funciona!"}' ``` Sucesso: ```json {"id":"3fa85f64-...","from":"nao-responda@seu-dominio-verificado.com","to":"voce@exemplo.com","created_at":"2026-01-15T12:00:00.000Z"} ``` Falha mais comum (HTTP 400): ```json {"error":{"message":"Domain seu-dominio-verificado.com is not verified for your account","type":"email_error"}} ``` ## Documentação - [Documentação da API](/docs) — referência para humanos - [Spec OpenAPI 3.0.3](/openapi.yaml) — legível por máquina, para codegen