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:latestNo 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, valeAMA4DEVS.callbackUrl: opcional, o endereço que recebe os eventos de status desta mensagem.
Consultar e apagar
| Método | Rota | Resposta |
|---|---|---|
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:
| Filtro | Valores | Descriçã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%2FstatusOs 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étodo | Rota | Resposta |
|---|---|---|
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âmetro | Apelidos | Descriçã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ódigo | HTTP | Onde | Significado |
|---|---|---|---|
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)──▶ deliveredCada 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(ouSMS_AUTO_DELIVER=0), a mensagem espera emqueuedaté alguém mudar o status na mão — é assim que se testa a espera do usuário. - Estados finais:
delivered,failedeundelivered. Uma vez alcançado, o temporizador não muda mais a mensagem; um status manual ou uma exclusão vence o relógio. - Falhas:
failedcarrega30003(Unreachable destination handset) eundeliveredcarrega30005(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:
| Final | Comportamento | Estado final | Có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
callbackUrlda própria mensagem quando ela tem um; senão, owebhookUrldas configurações. - Eventos de resposta: sempre o
webhookUrldas 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=1No formulário, Body só aparece no evento de resposta, e ErrorCode e ErrorMessage só aparecem quando há erro.
Cabeçalhos e assinatura
| Cabeçalho | Valor | Descriçã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 é
receivede 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 ContentLong 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=5000A 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ção | 1º segmento | Cada segmento seguinte | O 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-456vira123456.
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étodo | Rota | O 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
curle 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ável | Padrão | Significado |
|---|---|---|
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 na8899.
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:8899por padrão); o mock do Asaas o informa emGET /_admin/notify/status(sms.uiUrl).- Sem
SMS_MOCK_URL, o mock do Asaas procura o de SMS onde ele costuma responder: o serviçosmsdo Compose, o serviçosms-mock, esta máquina e o host do Docker — sempre na porta8899.
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çalhoHostcontra a lista permitida:localhost,127.0.0.1,::1,host.docker.internal,sms,sms-mocke os hosts extras deMOCK_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çalhoOrigin, quando ele vem. Uma origem de página externa é recusada; a origem opacanull(um iframe sandbox, uma páginafile:oudata:) também conta como estrangeira. - Feed e envio por GET: o handshake de
/wseGET /sms/sendtambém recusam umaOriginestrangeira 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, comvary: Origin. OOPTIONSde pré-flight responde 204 comaccess-control-allow-methods: GET,POST,PUT,DELETE,OPTIONSe os cabeçalhos pedidos de volta (ouauthorization,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
| Limite | Valor | Observaçã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 |