Docs

As três APIs de envio, o ciclo de vida de cada mensagem, os números mágicos que provocam falhas, os webhooks de status e de resposta, a contagem de segmentos e cada configuração.

APIs de envio

O mock expõe três formas de enviar uma mensagem. Todas compartilham o mesmo armazenamento, o mesmo ciclo de vida e as mesmas regras de validação: mudar de API muda o formato da requisição e da resposta, nunca o comportamento da mensagem.

A imagem é ghcr.io/instituto-ama/sms-mock e a porta do container é a 8899 (SMS_PORT no host):

docker run --rm -p 127.0.0.1:8899:8899 ghcr.io/instituto-ama/sms-mock:latest

No repositório, make up MOCKS=sms sobe o mesmo serviço pelo Compose, e o make up da raiz o traz junto com o mock do Asaas. O painel fica em http://localhost:8899.

API nativa

POST /v1/messages recebe JSON e devolve a mensagem criada com 201:

POST /v1/messages
Content-Type: application/json

{
  "to": "+5511912345678",
  "body": "Seu codigo de verificacao e 482913.",
  "from": "AMA4DEVS"
}

A resposta é a mensagem inteira, com o estado queued (o ciclo de vida ainda vai caminhar):

201 Created
Content-Type: application/json

{
  "id": "SM0123456789abcdef0123456789abcdef",
  "direction": "outbound",
  "from": "AMA4DEVS",
  "to": "+5511912345678",
  "body": "Seu codigo de verificacao e 482913.",
  "status": "queued",
  "api": "native",
  "encoding": "GSM-7",
  "segments": 1,
  "codes": ["482913"],
  "errorCode": null,
  "errorMessage": null,
  "callbackUrl": null,
  "createdAt": "2026-05-04T12:00:00.000Z",
  "updatedAt": "2026-05-04T12:00:00.000Z",
  "sentAt": null,
  "deliveredAt": null,
  "history": [
    { "status": "queued", "at": "2026-05-04T12:00:00.000Z" }
  ]
}
  • to: obrigatório, vira E.164 (+5511912345678). Sem código do país, um número de 10 ou 11 dígitos recebe o código padrão (55).
  • body: obrigatório, até 1600 caracteres.
  • from: opcional. Um número é normalizado; um identificador alfanumérico (AMA4DEVS) é guardado como está. Sem ele, vale AMA4DEVS.
  • callbackUrl: opcional, o endereço que recebe os eventos de status desta mensagem.

Consultar e apagar

MétodoRotaResposta
GET /v1/messages {"messages": [...], "count": 3} — a lista vem da mais antiga para a mais nova
GET /v1/messages/{id} A mensagem, ou 404 com not_found
DELETE /v1/messages/{id} 204, ou 404 com not_found

Os filtros da consulta (GET /v1/messages) combinam entre si:

FiltroValoresDescrição
to E.164 exato Só as mensagens enviadas para este número
from E.164 ou identificador alfanumérico Só as mensagens enviadas por este remetente
number E.164 exato Só as mensagens em que o número aparece de qualquer um dos dois lados
status queued, sent, delivered, undelivered, failed, received Só as mensagens naquele estado
direction outbound, inbound Só as enviadas pelo aplicativo ou só as respondidas pelo aparelho
q Texto Procura no corpo, nos dois números e nos códigos, sem diferenciar maiúsculas
limit Inteiro positivo Mantém as mensagens mais recentes (a ordem da mais antiga para a mais nova continua)
after Id de uma mensagem Só as mensagens criadas depois daquela
wait Milissegundos (até 30000) Espera por uma mensagem que combine com os filtros antes de responder

Os filtros de número comparam o valor exato guardado, por isso o + do E.164 precisa ser codificado na consulta: ?to=%2B5511912345678.

API compatível com Twilio

POST /2010-04-01/Accounts/{AccountSid}/Messages.json recebe o formulário que o SDK da Twilio envia e devolve 201 no formato da Twilio. A autorização é ignorada: qualquer Basic serve, e nenhuma também.

POST /2010-04-01/Accounts/AC00000000000000000000000000000000/Messages.json
Content-Type: application/x-www-form-urlencoded

To=%2B5511912345678&From=AMA4DEVS&Body=Seu+codigo+e+482913.&StatusCallback=http%3A%2F%2Flocalhost%3A4545%2Fsms%2Fstatus

Os campos do formulário são To, From, Body e StatusCallback; MessagingServiceSid no lugar de From usa o remetente padrão. A resposta:

201 Created

{
  "sid": "SM0123456789abcdef0123456789abcdef",
  "account_sid": "AC00000000000000000000000000000000",
  "api_version": "2010-04-01",
  "body": "Seu codigo e 482913.",
  "date_created": "Mon, 04 May 2026 12:00:00 +0000",
  "date_updated": "Mon, 04 May 2026 12:00:00 +0000",
  "date_sent": null,
  "direction": "outbound-api",
  "error_code": null,
  "error_message": null,
  "from": "AMA4DEVS",
  "to": "+5511912345678",
  "num_segments": "1",
  "num_media": "0",
  "price": null,
  "price_unit": "USD",
  "status": "queued",
  "messaging_service_sid": null,
  "uri": "/2010-04-01/Accounts/AC00000000000000000000000000000000/Messages/SM0123456789abcdef0123456789abcdef.json",
  "subresource_uris": {
    "media": "/2010-04-01/Accounts/AC00000000000000000000000000000000/Messages/SM0123456789abcdef0123456789abcdef/Media.json"
  }
}

As datas usam o formato RFC 2822, como na Twilio. A leitura e a exclusão também existem:

MétodoRotaResposta
GET /2010-04-01/Accounts/{sid}/Messages.json?To=&From=&PageSize= {"messages": [...], "page": 0, "page_size": 50, ...} — da mais nova para a mais antiga, PageSize de 50 por padrão e no máximo 1000
GET /2010-04-01/Accounts/{sid}/Messages/{messageSid}.json A mensagem, ou 404 com o código 20404
DELETE /2010-04-01/Accounts/{sid}/Messages/{messageSid}.json 204, ou 404 com o código 20404

API simples

GET|POST /sms/send é o caminho mais curto para mandar uma mensagem. Os parâmetros chegam na consulta (no GET) ou no corpo (no POST, em formulário ou JSON):

ParâmetroApelidosDescrição
to — O destinatário, normalizado para E.164
message text, body O texto da mensagem
sender from O remetente; sem ele, vale AMA4DEVS
callbackUrl — O endereço que recebe os eventos de status desta mensagem

A resposta é 200, curta e sem o corpo da mensagem:

GET /sms/send?to=%2B5511912345678&message=Seu+codigo+e+482913.

200 OK

{
  "status": "ok",
  "id": "SM0123456789abcdef0123456789abcdef",
  "to": "+5511912345678",
  "segments": 1
}

Erros

A API nativa e a simples respondem com o mesmo formato de erro (400):

{
  "error": {
    "code": "invalid_to",
    "message": "Invalid 'To' phone number: use E.164, e.g. +5511912345678.",
    "field": "to"
  }
}

A API simples troca o invólucro por {"status":"error","code":"...","message":"..."} e mantém o mesmo código de erro. A compatível com Twilio usa o formato dela: {"code": 21211, "message": "...", "more_info": "...", "status": 400}.

CódigoHTTPOndeSignificado
invalid_to 400 nativa, simples O número não virou E.164
rejected_number 400 nativa, simples O número é o mágico 0003, que a API recusa
empty_body 400 nativa, simples O corpo veio vazio
body_too_long 400 nativa, simples O corpo passa de 1600 caracteres
invalid_from 400 nativa, simples O remetente não é E.164 nem identificador alfanumérico
invalid_json 400 nativa, controle O corpo não é JSON válido
not_found 404 nativa, controle O id não existe no armazenamento
method_not_allowed 405 todas Aquele método não responde naquela rota
21211 400 Twilio To inválido (inclui o número mágico 0003)
21212 400 Twilio From inválido
21602 400 Twilio O corpo é obrigatório
21617 400 Twilio O corpo passa de 1600 caracteres
20404 404 Twilio A mensagem não existe

Ciclo de vida

Toda mensagem enviada começa em queued e caminha sozinha. Com o atraso padrão (SMS_DELIVERY_DELAY_MS, 1500 ms), a vida de uma mensagem normal é:

queued  ──(atraso / 3)──▶  sent  ──(resto do atraso)──▶  delivered

Cada mudança de estado vira uma entrada no histórico e dispara um webhook de status:

"history": [
  { "status": "queued", "at": "2026-05-04T12:00:00.000Z" },
  { "status": "sent", "at": "2026-05-04T12:00:00.500Z" },
  { "status": "delivered", "at": "2026-05-04T12:00:01.500Z" }
],
"sentAt": "2026-05-04T12:00:00.500Z",
"deliveredAt": "2026-05-04T12:00:01.500Z"
  • deliveryDelayMs: o tempo total da vida. Com 0, os passos acontecem na hora, de uma vez.
  • autoDeliver: com false (ou SMS_AUTO_DELIVER=0), a mensagem espera em queued até alguém mudar o status na mão — é assim que se testa a espera do usuário.
  • Estados finais: delivered, failed e undelivered. Uma vez alcançado, o temporizador não muda mais a mensagem; um status manual ou uma exclusão vence o relógio.
  • Falhas: failed carrega 30003 (Unreachable destination handset) e undelivered carrega 30005 (Unknown destination handset), salvo se o código vier na mão.

Mudar o status na mão

POST /_mock/sms/messages/{id}/status move uma mensagem enviada pelo aplicativo para sent, delivered, undelivered ou failed:

POST /_mock/sms/messages/SM0123456789abcdef0123456789abcdef/status
Content-Type: application/json

{
  "status": "failed",
  "errorCode": 30003
}

A resposta é a mensagem atualizada. Uma resposta recebida pelo aparelho não tem status a mudar: o mock devolve 409 com not_outbound.

Números mágicos

Cinco finais de número provocam um desfecho predetermined, para o teste não depender de sorte. O desfecho vem dos quatro últimos dígitos do número já normalizado:

FinalComportamentoEstado finalCódigo
0001 A entrega falha: o aparelho está fora de alcance failed 30003
0002 O destino é desconhecido undelivered 30005
0003 A API recusa o envio: nenhuma mensagem é criada e a resposta é 400 — 21211
0004 A mensagem fica parada em sent, como se a operadora nunca confirmasse sent —
0005 A operadora filtra a mensagem failed 30007

Qualquer outro número entrega normalmente. Exemplo: +5511912340001 falha na entrega, +5511912340003 nem chega a ser criada.

Webhooks

O mock avisa a sua aplicação sobre cada mudança de estado e sobre cada resposta recebida. O endereço sai de duas fontes:

  • Eventos de status: o callbackUrl da própria mensagem quando ela tem um; senão, o webhookUrl das configurações.
  • Eventos de resposta: sempre o webhookUrl das configurações.

O corpo depende da API por onde a mensagem entrou. Nas APIs nativa, simples e de controle, é JSON:

{
  "event": "message.status",
  "message": {
    "id": "SM0123456789abcdef0123456789abcdef",
    "direction": "outbound",
    "from": "AMA4DEVS",
    "to": "+5511912345678",
    "body": "Seu codigo de verificacao e 482913.",
    "status": "delivered",
    "api": "native",
    "encoding": "GSM-7",
    "segments": 1,
    "codes": ["482913"],
    "errorCode": null,
    "errorMessage": null,
    "callbackUrl": null,
    "createdAt": "2026-05-04T12:00:00.000Z",
    "updatedAt": "2026-05-04T12:00:01.500Z",
    "sentAt": "2026-05-04T12:00:00.500Z",
    "deliveredAt": "2026-05-04T12:00:01.500Z",
    "history": [
      { "status": "queued", "at": "2026-05-04T12:00:00.000Z" },
      { "status": "sent", "at": "2026-05-04T12:00:00.500Z" },
      { "status": "delivered", "at": "2026-05-04T12:00:01.500Z" }
    ]
  }
}

O evento é message.status para as mudanças de estado e message.inbound para as respostas. Nas mensagens que entraram pela API compatível com Twilio, o corpo é o formulário que a Twilio manda:

MessageSid=SM0123456789abcdef0123456789abcdef&SmsSid=SM0123456789abcdef0123456789abcdef&AccountSid=ACmock00000000000000000000000000&MessageStatus=delivered&SmsStatus=delivered&From=AMA4DEVS&To=%2B5511912345678&NumSegments=1

No formulário, Body só aparece no evento de resposta, e ErrorCode e ErrorMessage só aparecem quando há erro.

Cabeçalhos e assinatura

CabeçalhoValorDescrição
content-type application/json ou application/x-www-form-urlencoded Depende da API da mensagem
user-agent sms-mock Identifica o mock
x-sms-event status ou inbound O tipo do evento
x-sms-signature sha256=<hexadecimal> HMAC-SHA256 do corpo exato, quando há SMS_WEBHOOK_SECRET

A assinatura é o HMAC-SHA256 do corpo, em hexadecimal, calculado com o segredo configurado. Para conferir, o segredo e o corpo precisam ser exatamente os mesmos que saíram.

Loopback dentro do container

Quando o mock roda em um container, localhost é o próprio container. Com WEBHOOK_LOCALHOST_REWRITE definido, um endereço de loopback (localhost, 127.0.0.1, ::1) é reescrito para esse host antes da requisição — por exemplo, host.docker.internal.

Falhas e log

Um erro de rede ou uma resposta fora da faixa 2xx nunca derruba o mock: a tentativa fica registrada com ok: false e o erro ao lado. O log guarda as 200 últimas tentativas e cada uma tem o corpo enviado, o tipo de conteúdo, o código de resposta e se foi assinada:

GET /_mock/sms/webhooks/deliveries

{
  "deliveries": [
    {
      "id": "SM0123456789abcdef0123456789abcdef",
      "messageId": "SM0123456789abcdef0123456789abcdef",
      "event": "status",
      "url": "http://localhost:4545/sms/status",
      "ok": true,
      "statusCode": 200,
      "error": null,
      "requestBody": "{\"event\":\"message.status\",...}",
      "contentType": "application/json",
      "signed": true,
      "at": "2026-05-04T12:00:01.600Z"
    }
  ]
}

DELETE /_mock/sms/webhooks/deliveries limpa o log.

Respostas recebidas

No mock, o aparelho é o painel: os SMS que o aplicativo envia aparecem na conversa à esquerda, com o remetente, o chip do código de OTP para copiar, o selo de segmentos e o status (na fila, enviada, entregue, não entregue, falhou). A gaveta de detalhes — o ícone de informação ou um clique na mensagem — mostra os campos, o JSON e o histórico de status; o que se escreve no rodapé sai como resposta do aparelho. Pela perspectiva da API, isso são duas direções:

  • outbound: o aplicativo manda, o aparelho recebe. É o que as APIs de envio criam.
  • inbound: o aparelho responde, o aplicativo recebe. O estado é received e a API de origem é mock.

Além do painel, a resposta pode vir pela rota de controle:

POST /_mock/sms/inbound
Content-Type: application/json

{
  "from": "+5511912345678",
  "to": "AMA4DEVS",
  "body": "Recebi, obrigado!"
}

A resposta é 201 com a mensagem criada. Sem to, o destino é o remetente padrão das configurações. Cada resposta dispara o webhook message.inbound.

Testes

O armazenamento é em memória, então cada teste começa limpo de dois jeitos: reiniciando o container (some tudo) ou chamando a rota de reset (some as mensagens e as entregas de webhook, sem derrubar o serviço).

POST /_mock/sms/reset

204 No Content

Long poll

Em vez de ficar consultando, a consulta espera. Com wait, a resposta só volta quando uma mensagem que combine com os filtros aparece (ou quando o tempo acaba, com a lista vazia):

GET /v1/messages?to=%2B5511912345678&wait=5000

A espera é de no máximo 30000 ms. Se já existe uma mensagem que combina com o filtro, a resposta é imediata. Para esperar apenas o que vier depois de um ponto conhecido, use after com o id da última mensagem vista.

Receita de CI

Um teste de ponta a ponta, com o mock rodando em http://localhost:8899:

# 1. limpa o estado entre os testes
curl -s -X POST http://localhost:8899/_mock/sms/reset

# 2. envia a mensagem
curl -s -X POST http://localhost:8899/v1/messages \
  -H 'content-type: application/json' \
  -d '{"to":"+5511912345678","body":"Seu codigo e 482913."}'

# 3. espera a mensagem chegar (long poll de 5 s)
curl -s 'http://localhost:8899/v1/messages?to=%2B5511912345678&wait=5000'

Para conferir o resultado, o campo codes da mensagem traz o código extraído do texto (["482913"]), e o filtro q encontra a mensagem pelo código, pelo corpo ou pelo número.

Para começar com conteúdo na tela, POST /_mock/sms/demo semeia uma caixa de entrada de exemplo: um OTP, um aviso de entrega, uma promoção longa em UCS-2, uma mensagem para o número que sempre falha e uma resposta do aparelho.

Segmentos e OTP

Toda mensagem é cobrada por segmento, e o mock calcula a conta como a operadora:

Codificação1º segmentoCada segmento seguinteO que conta
GSM-7 160 septetos 153 septetos Os caracteres da tabela básica; os da extensão (^ { } \ [ ] ~ | €) custam dois septetos cada
UCS-2 70 unidades UTF-16 67 unidades UTF-16 Qualquer caractere fora da GSM-7; um emoji conta duas unidades

A mensagem traz a conta pronta nos campos encoding e segments. Uma mensagem de 160 caracteres caber em um segmento; a de 161, não cabe e vira dois. Um texto vazio teria zero segmentos, mas o corpo é obrigatório em todas as APIs de envio.

Códigos de verificação

O mock lê o corpo e separa os códigos que o usuário vai precisar digitar:

  • Uma sequência de 4 a 8 dígitos que não esteja grudada em outros dígitos: 482913.
  • Dois grupos de 3 ou 4 dígitos separados por hífen ou espaço, que viram um código só: 123-456 vira 123456.

Os códigos repetidos aparecem uma vez só e na ordem em que o texto traz. Eles ficam no campo codes da mensagem e são encontrados pelo filtro q.

O painel

Abra http://localhost:8899: o painel é o celular do teste. Ele abre em português; a chave PT|EN no cabeçalho troca o idioma, e os nomes abaixo aparecem nos dois.

A caixa de entrada

  • O cabeçalho: a marca do SMS, o estado ao vivo, a chave de idioma, um ícone para uma nova mensagem de teste e um ícone para o painel de controle.
  • A barra lateral: um seletor de remetente, a busca, os filtros Tudo, Sem resposta e Com falha, e uma conversa por número de telefone.
  • A conversa: os SMS que a aplicação mandou são as bolhas da esquerda, com o remetente, o chip que copia o código de verificação, o selo de segmentos (1 SMS · UCS-2) e o status (na fila, enviada, entregue, não entregue, falhou). Ao passar o mouse, cada mensagem pode ser entregue (Entregar), falhada (Falhar), marcada como não entregue (Não entregue) ou apagada (Apagar) na mão.
  • O campo de resposta: o que você digita ali é o que o celular envia: chega como a bolha da direita e dispara o webhook de entrada.
  • A gaveta de detalhes: o ícone de informação, ou um clique numa bolha, abre os campos da mensagem, o JSON dela e o histórico de status.
  • A tela vazia: Nova mensagem de teste, Carregar exemplos e Abrir painel de controle.

O painel de controle

O ícone do cabeçalho abre o painel de controle, com uma aba por assunto:

Aba O que faz
Entrega Entrega automática ou manual, o atraso até a entrega em milissegundos e o botão que recomeça do zero.
Webhook A URL para onde os eventos vão, o segredo que os assina, o remetente padrão, o formato dos corpos de status e de entrada, o cabeçalho x-sms-signature e um trecho em Node para conferir a assinatura.
Eventos Cada tentativa de webhook, com o corpo, o código de resposta e a hora.
Números mágicos Os finais que provocam uma falha, com as regras dos segmentos e do OTP.
API O catálogo da API: 20 operações, com parâmetros, campos do corpo, um curl, cada resposta documentada e um botão Testar que envia a requisição de verdade e mostra a resposta.

Rotas de controle

As rotas de controle mudam o mock em tempo de execução. Todas vivem sob /_mock/sms/ e passam pela proteção local:

MétodoRotaO que faz
GET /_mock/sms/settings Lê as configurações em uso
PUT /_mock/sms/settings Muda parte das configurações; um valor de tipo errado recusa a mudança com 400 e invalid_settings
POST /_mock/sms/inbound Responde como o aparelho; devolve 201 com a mensagem
POST /_mock/sms/messages/{id}/status Muda o status de uma mensagem na mão
POST /_mock/sms/reset Apaga as mensagens e as entregas; devolve 204
GET /_mock/sms/webhooks/deliveries Lista as tentativas de webhook
DELETE /_mock/sms/webhooks/deliveries Limpa o log de entregas; devolve 204
POST /_mock/sms/demo Semeia uma caixa de entrada de exemplo; devolve 201 com as mensagens
GET /health A única rota sem proteção: {"status":"ok","service":"sms","version":"0.2.0"}
GET /ws WebSocket do painel ao vivo: o primeiro quadro é o snapshot (mensagens, configurações e entregas) e depois vêm os eventos

Configurações em tempo de execução

GET /_mock/sms/settings devolve exatamente o que o mock está usando:

{
  "deliveryDelayMs": 1500,
  "autoDeliver": true,
  "webhookUrl": "",
  "webhookSecret": "",
  "defaultFrom": "AMA4DEVS"
}

Um PUT parcial muda só o que vem no corpo:

PUT /_mock/sms/settings
Content-Type: application/json

{
  "deliveryDelayMs": 0,
  "autoDeliver": false
}

O painel de controle

O ícone de ajustes do painel abre o painel de controle, em tela cheia, com as abas na lateral. Cada aba é um grupo das rotas acima, com o formulário em vez do curl:

  • Entrega: automática ou manual, o atraso até a entrega em milissegundos e o botão de começar do zero, que apaga todas as conversas e todos os eventos de webhook do mock.
  • Webhook: o endereço, o segredo e o remetente padrão, com o formato do payload de cada evento e o header x-sms-signature, mais um trecho em Node para conferir a assinatura.
  • Eventos: cada tentativa de webhook, da mais nova para a mais antiga, com o corpo enviado, o código de resposta e a marca de assinado.
  • Números mágicos: o desfecho de cada final de número, os limites de segmento no GSM-7 e no UCS-2 e o tamanho do código de OTP.
  • API: o catálogo inteiro da API — 20 operações, com os parâmetros, os campos, um curl e todas as respostas documentadas — e um botão Testar que manda a requisição de verdade e mostra a resposta do mock.

Variáveis de ambiente

VariávelPadrãoSignificado
PORT 8899 Porta do servidor HTTP do mock
MOCK_ALLOWED_HOSTS (vazio) Hosts extras aceitos pela proteção local, separados por vírgula
SMS_WEBHOOK_URL (vazio) Endereço que recebe os eventos de status e de resposta
SMS_WEBHOOK_SECRET (vazio) Segredo do HMAC-SHA256 da assinatura; vazio significa webhook sem assinatura
SMS_DELIVERY_DELAY_MS 1500 Tempo total do ciclo de vida da mensagem; 0 faz tudo na hora
SMS_AUTO_DELIVER 1 Com 0, as mensagens ficam em queued esperando um status manual
SMS_DEFAULT_COUNTRY_CODE 55 Código do país usado nos números escritos sem um
SMS_DEFAULT_FROM AMA4DEVS Remetente padrão quando a requisição não informa nenhum
SMS_MAX_MESSAGES 5000 Quantidade de mensagens guardadas em memória; as mais antigas são descartadas
WEBHOOK_LOCALHOST_REWRITE (vazio) Host que substitui o loopback nos endereços de webhook (do container para a máquina)

Depois de iniciado, as mesmas configurações podem ser mudadas em tempo de execução por PUT /_mock/sms/settings ou pelo painel de controle, nas abas Entrega e Webhook.

Com o mock do Asaas

Neste repositório o mock de SMS sobe junto com o do Asaas: o make up da raiz (o perfil asaas do Compose) traz o mock do Asaas, o gerador, o Mailpit e o mock de SMS, cada um na sua porta. Duas variáveis do .env mudam isso:

  • SMS_ENABLED=0: mantém o mock de SMS desligado. O mock do Asaas então não encontra nenhum e não manda SMS ao pagador.
  • SMS_PORT: a porta do host; o contêiner sempre escuta na 8899.

Do lado do Asaas, o mock de SMS é encontrado sozinho:

  • SMS_MOCK_URL: o endereço do mock de SMS. Vazio, o SMS do pagador é desligado.
  • SMS_UI_URL: o endereço do painel do mock de SMS como o navegador o abre (http://localhost:8899 por padrão); o mock do Asaas o informa em GET /_admin/notify/status (sms.uiUrl).
  • Sem SMS_MOCK_URL, o mock do Asaas procura o de SMS onde ele costuma responder: o serviço sms do Compose, o serviço sms-mock, esta máquina e o host do Docker — sempre na porta 8899.

Encontrado, ele manda para o mock de SMS os avisos de SMS do pagador (POST /v1/messages, com o remetente ASAAS), nos mesmos eventos de cobrança que avisam o e-mail e o WhatsApp, e só para quem tem celular cadastrado.

Proteção local (Local Guard)

Como o mock não exige chave de API, ele se protege pelo endereço de onde a requisição vem, para não ficar exposto na rede da máquina do desenvolvedor:

  • Host: todas as rotas, exceto /health, validam o cabeçalho Host contra a lista permitida: localhost, 127.0.0.1, ::1, host.docker.internal, sms, sms-mock e os hosts extras de MOCK_ALLOWED_HOSTS. Isso bloqueia DNS rebinding, em que uma página resolve um nome para 127.0.0.1 mas continua mandando o nome no cabeçalho.
  • Origem: as requisições que mudam o estado (POST, PUT, PATCH, DELETE) validam também o cabeçalho Origin, quando ele vem. Uma origem de página externa é recusada; a origem opaca null (um iframe sandbox, uma página file: ou data:) também conta como estrangeira.
  • Feed e envio por GET: o handshake de /ws e GET /sms/send também recusam uma Origin estrangeira ou uma requisição de navegador entre sites (Sec-Fetch-Site: cross-site); assim, uma página que você visita não lê o feed ao vivo nem dispara um envio.
  • CORS: quando a origem é permitida, ela é refletida em access-control-allow-origin, com vary: Origin. O OPTIONS de pré-flight responde 204 com access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS e os cabeçalhos pedidos de volta (ou authorization,content-type, quando o navegador não nomeia nenhum). Uma origem estrangeira no pré-flight leva 403.

As recusas são 403 com o código forbidden_host ou forbidden_origin.

A API nativa aceita qualquer Bearer e a compatível com Twilio aceita qualquer Basic (ou nenhum): a chave não é conferida, porque o mock existe para ser chamado sem segredo.

Limites

LimiteValorObservação
Corpo da mensagem 1600 caracteres Dez segmentos concatenados; acima disso, body_too_long
Espera da consulta 30000 ms O wait é limitado a 30 segundos
Mensagens em memória 5000 SMS_MAX_MESSAGES; as mais antigas são descartadas
Log de webhooks 200 tentativas As mais antigas são descartadas
Tempo do webhook 5 s Acima disso, a requisição é abortada e registrada como falha
PageSize da Twilio 50 por padrão, 1000 no máximo A lista vem da mensagem mais nova para a mais antiga
Números em E.164 8 a 15 dígitos Sem código do país, 10 ou 11 dígitos recebem o código 55
Remetente alfanumérico Até 11 caracteres Letra no começo, depois letras, dígitos ou espaços
Chave de API Nenhuma A API nativa aceita qualquer Bearer; a da Twilio, qualquer Basic
Armazenamento Em memória Reiniciar o container apaga as mensagens, as respostas e o log de webhooks