SMS, com você no celular.

Um gateway de SMS no localhost. A sua aplicação envia pela API nativa, pela compatível com Twilio ou por um GET simples, e a mensagem cai no painel, que faz o papel do celular. Você responde, copia o código e escolhe a falha: os números terminados em 0001 a 0005 reproduzem os erros da operadora. Cada status e cada resposta voltam como webhook.

docker run --rm -p 127.0.0.1:8899:8899 ghcr.io/instituto-ama/sms-mock:latest
MOCK SMS
reprodução de uma execução
  1. 29/10/2026
  2. POST /v1/messages 201
    AMA → +5511912340010
    "Seu código Ama é 482913"
    UCS-2, 1 segmento, código 482913
  3. webhook message.status queued
  4. webhook message.status sent
  5. webhook message.status delivered
  6. POST /_mock/sms/inbound 201
    +5511912340010 → AMA4DEVS
    "CONFIRMAR"
  7. webhook message.inbound received
    "CONFIRMAR"
  8. POST /v1/messages 201
    AMA → +5511912340001
    "Seu código Ama é 730144"
  9. webhook message.status sent
  10. webhook message.status failed
  11. (30003) Unreachable destination handset

Uma execução real da imagem: o código sai pela API e o webhook acompanha cada status, a resposta do celular volta como webhook e o final 0001 falha com o código 30003 da operadora. O acento de "código" já põe a mensagem em UCS-2. Só o papel é desenhado.

Em qual porta roda.

O mock de SMS responde na porta 8899: o painel, a API e o feed ao vivo usam a mesma. Fica preso em 127.0.0.1, então só a sua máquina chega nele.

O quêEndereçoComo mudar
Painel, API e /wshttp://localhost:8899SMS_PORT no .env troca a porta do host; o contêiner sempre escuta na 8899
Health checkhttp://localhost:8899/healtha mesma porta
Com docker run-p 127.0.0.1:8899:8899outra porta no host: -p 127.0.0.1:8898:8899
Com make up MOCKS=sms127.0.0.1:${SMS_PORT:-8899}SMS_PORT=8898 no .env
Sem Dockerbun src/server.ts em mocks/smsPORT=8898 no ambiente

Os outros mocks mantêm as suas portas, então rodam lado a lado: WhatsApp 3010, Asaas 45445, gerador 45450, Mailpit 8025. Se outro serviço já usa a 8899, publique o mock de SMS em uma porta livre do host e aponte a sua aplicação para ela.

Sobe junto com o mock do Asaas.

Um comando só na raiz do repositório já deixa os dois mocks no ar: o do Asaas, o gerador, o Mailpit e o de SMS, cada um na sua porta. O mock do Asaas encontra o de SMS sozinho e manda para ele os SMS do pagador.

O quêComo fica
make up na raizSobe o perfil asaas: o mock do Asaas, o gerador, o Mailpit e o mock de SMS
SMS_ENABLED=0 no .envMantém o mock de SMS desligado; o do Asaas então não encontra nenhum e não manda SMS
make up MOCKS=smsSobe só o mock de SMS, na porta 8899
Do lado do AsaasSMS_MOCK_URL aponta para o mock de SMS; vazio, o SMS do pagador é desligado
Endereço do painel, como o navegador o abreSMS_UI_URL, no mock do Asaas; por padrão http://localhost:8899
Onde o mock do Asaas procuraSozinho: o serviço sms do Compose, esta máquina e o host do Docker, sempre na porta 8899

O mock do Asaas (versão 0.11.0) manda os SMS do pagador para a API do mock de SMS — 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. Nada a configurar: se o mock de SMS estiver no ar, ele é encontrado.

Uma execução verificada: uma cobrança de "Maria Souza" produziu o SMS ASAAS → +5511987654321 — "Maria Souza: Conta Principal gerou uma cobrança de R$ 120,50 (Consulta), com vencimento em 10/11/2026. Ver fatura e pagar: …" — que terminou delivered no painel do mock de SMS.

O painel do mock de SMS: na conversa de Maria Souza, a cobrança mandada pelo mock do Asaas, com o remetente ASAAS, o selo de 3 SMS em UCS-2 e o status entregue.

O que ele entrega.

O comportamento de uma operadora, sem a operadora: o que a sua aplicação envia, o que chega, quanto custa e o que falha.

Recurso O que faz Na prática
Ciclo de vida A mensagem sai de queued, passa por sent e chega a delivered — ou termina em failed e undelivered sent em 500 ms, delivered em 1500 ms
Números mágicos Os quatro últimos dígitos do destino decidem o desfecho da mensagem final 0001 → failed com código 30003
Extração de OTP Varre o corpo e encontra o código de verificação, de 4 a 8 dígitos "código 482913" → codes: ["482913"]
Segmentos Conta as partes cobradas do texto, em GSM-7 ou UCS-2 160 e 153 caracteres no GSM-7, 70 e 67 no UCS-2
API compatível com Twilio O mesmo caminho, os mesmos corpos de formulário e os mesmos códigos de erro do Twilio POST /2010-04-01/Accounts/{sid}/Messages.json
Webhooks Uma chamada a cada mudança de status e outra quando o aparelho responde, com assinatura HMAC x-sms-signature: sha256=…
Long poll A listagem espera a mensagem chegar, para o teste não precisar de sleep GET /v1/messages?wait=5000
Painel ao vivo A caixa do aparelho: uma conversa por número, o código de OTP em um chip que copia, o selo de segmentos e o status de cada mensagem ws://localhost:8899/ws
Painel de controle Entrega automática ou manual, webhook, eventos, números mágicos e a API inteira, com um botão que roda cada operação de verdade o ícone de ajustes do painel

Números mágicos.

O destino decide o desfecho pelos quatro últimos dígitos. O mesmo número falha em qualquer máquina, em qualquer teste.

Final do número Desfecho Status final Código Resposta HTTP
0001 Handset inalcançável failed 30003 201
0002 Handset desconhecido undelivered 30005 201
0003 Rejeitado pela API, na hora do envio — 21211 400
0004 Parece enviado e nunca confirma a entrega sent — 201
0005 Filtrado pela operadora failed 30007 201
qualquer outro Entrega normal delivered — 201

Casos de uso.

Quatro problemas que aparecem na hora de integrar qualquer envio de SMS.

Testar um login com OTP

O código sai do texto, não de um regex no seu teste: o mock separa a sequência de 4 a 8 dígitos do corpo, guarda em codes e mostra um chip que copia o código no painel. Aponte a sua aplicação para o mock, mande o código para um número fixo e o teste acompanha a entrega como ela acontece: enfileirado, enviado, entregue.

curl -X POST http://localhost:8899/v1/messages \
  -H 'Content-Type: application/json' \
  -d '{"to": "+5511912340010", "body": "Seu código de verificação é 482913"}'
A conversa de +55 11 91234-0010 no painel: o SMS do BANCO com o código 482913 em um chip para copiar e a resposta escrita como o celular.
O código de verificação chega como um chip: um clique copia os dígitos para o teste.

Testar os webhooks

Aponte SMS_WEBHOOK_URL para a sua aplicação e receba message.status a cada mudança de status e message.inbound quando o aparelho responde. Com SMS_WEBHOOK_SECRET cada chamada vem com o header x-sms-signature: sha256=<hmac> do corpo, para você validar a assinatura como faria em produção. A aba Webhook do painel de controle mostra o endereço, o segredo e o remetente padrão; a aba Eventos mostra cada tentativa, com o corpo enviado e o código de resposta.

docker run --rm -p 127.0.0.1:8899:8899 \
  -e SMS_WEBHOOK_URL=http://host.docker.internal:4000/webhooks/sms \
  -e SMS_WEBHOOK_SECRET=my-app-secret \
  ghcr.io/instituto-ama/sms-mock:latest

curl http://localhost:8899/_mock/sms/webhooks/deliveries
A aba Webhook do painel de controle: o endereço do webhook, o segredo HMAC e o remetente padrão de um lado, os formatos do payload do outro.
O endereço, o segredo e o remetente padrão de um lado; o corpo que chega na sua aplicação, do outro.
A aba Eventos do painel de controle: as tentativas de webhook mais novas, cada uma com o código de resposta, o endereço, a marca de assinado e o corpo enviado.
Cada tentativa de webhook fica registrada com o corpo, o código de resposta e a marca de assinado.

A API, dentro do painel

Prova o seu cliente contra o contrato de verdade: o painel de controle carrega o catálogo inteiro da API — 20 operações, com os parâmetros, os campos, um curl e todas as respostas documentadas — e cada operação tem um botão Testar que manda a requisição de verdade e mostra a resposta que voltou. Para conferir um parâmetro, não é preciso sair do navegador.

curl -X POST http://localhost:8899/v1/messages \
  -H 'content-type: application/json' \
  -d '{"to":"+5511912340010","body":"Seu código de verificação é 482913","from":"AMA4DEVS"}'
A aba API do painel de controle: a operação de envio nativo aberta, com o exemplo, os códigos de resposta e o botão Testar depois de uma chamada de verdade.
O botão Testar manda a operação tal e qual e mostra a resposta do mock, com o código HTTP.

Testar as falhas de entrega

O número que termina em 0001 sempre falha com o código 30003, o 0002 fica undelivered com 30005 e o 0004 nunca passa de sent. Com isso o seu tratamento de erro é exercitado com o mesmo número, na sua máquina e no CI, sem depender de uma operadora real. A aba Números mágicos do painel de controle traz a tabela dos desfechos, os limites de segmento e o tamanho do código de OTP.

curl -X POST http://localhost:8899/v1/messages \
  -H 'Content-Type: application/json' \
  -d '{"to": "+5511912340001", "body": "Teste de falha na entrega"}'
A aba Números mágicos do painel de controle: a tabela com o desfecho de cada final de número, ao lado dos limites de segmento e do tamanho do código de OTP.
O final do número decide o desfecho; a tabela do painel mostra o código de cada caso.

Mover a entrega na mão

Prova a sua aplicação quando o aparelho demora: com a entrega no modo manual, a mensagem fica na fila esperando, e quem decide quando ela sai, quando chega e quando falha é você. A aba Entrega do painel de controle liga e desliga o modo automático e ajusta o atraso em milissegundos; na conversa, cada bolha também anda sozinha — Entregar, Falhar, Não entregue.

ID=$(curl -s -X POST http://localhost:8899/v1/messages \
  -H 'Content-Type: application/json' \
  -d '{"to": "+5511912340010", "body": "Teste manual"}' | grep -o '"id": *"[^"]*"' | head -1 | cut -d'"' -f4)
curl -X POST "http://localhost:8899/_mock/sms/messages/$ID/status" \
  -H 'Content-Type: application/json' \
  -d '{"status": "failed", "errorCode": 30003}'
A aba Entrega do painel de controle: o modo automático ou manual, o atraso até a entrega em milissegundos e o botão de começar do zero.
Automática ou manual, com o atraso em milissegundos e um botão para começar do zero.

Apontar um SDK da Twilio para o mock

O caminho /2010-04-01/Accounts/{sid}/Messages.json, os corpos de formulário e os códigos de erro são os do Twilio. Troque a URL base do cliente e o seu código de produção continua igual; qualquer credencial é aceita, porque o mock não valida token.

curl -X POST http://localhost:8899/2010-04-01/Accounts/ACmock00000000000000000000000000/Messages.json \
  -u ACmock00000000000000000000000000:mock-auth-token \
  -d "To=+5511912340010" \
  -d "From=AMA4DEVS" \
  -d "Body=Seu codigo e 482913"

A API numa tabela.

Três interfaces de envio, uma de controle e um WebSocket. Tudo em memória, tudo no seu localhost.

Método e rota Descrição Resposta de sucesso
GET /health Verificação de saúde do serviço {"status": "ok", "service": "sms", "version": "..."}
POST /v1/messages Cria e enfileira uma mensagem de saída 201 com a mensagem
GET /v1/messages Lista as mensagens por filtro, com long poll em wait {"messages": [...], "count": 1}
GET /v1/messages/:id Busca uma mensagem pelo identificador Mensagem
DELETE /v1/messages/:id Apaga uma mensagem 204
POST /2010-04-01/Accounts/:sid/Messages.json Envio no formato do Twilio 201 com o recurso do Twilio
GET|POST /sms/send Envio simples, por query ou por corpo {"status": "ok", "id": "...", "to": "...", "segments": 1}
POST /_mock/sms/inbound Resposta escrita pelo aparelho 201 com a mensagem recebida
POST /_mock/sms/messages/:id/status Muda o status de uma mensagem de saída Mensagem
GET|PUT /_mock/sms/settings Lê e muda os ajustes em tempo real {"deliveryDelayMs": 1500, "autoDeliver": true, ...}
GET /_mock/sms/webhooks/deliveries Lista as tentativas de webhook {"deliveries": [...]}
POST /_mock/sms/demo Popula a caixa de exemplo do painel 201 com as mensagens criadas
GET /ws WebSocket com o estado e os eventos do mock Quadros de evento