Quick start

From docker run to OTP verification, failure handling, and Twilio integration. Every command below runs as written against a fresh container.

  1. Run it

    Run the SMS mock container in Docker. In this repository, the root make up brings it up together with the Asaas mock, the generator and Mailpit; make up MOCKS=sms starts only the SMS mock.

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

    The mock listens on port 8899. Verify that the server is ready:

    curl http://localhost:8899/health
    {
      "status": "ok",
      "service": "sms",
      "version": "0.2.0"
    }
  2. Open the panel

    Open http://localhost:8899 in your browser. The panel is the handset: the SMS your application sends arrive as the bubbles on the left, with the sender id, the chip that copies the verification code, the segment count and the status (queued, sent, delivered, undelivered, failed). What you type in the composer is what the handset sends: it arrives as the bubble on the right and fires the inbound webhook. The details drawer — the info icon, or a click on a bubble — shows the fields of the message, its JSON and its status history. The panel opens in Brazilian Portuguese; the PT|EN switch in the header changes the language.

    The panel with the conversation of +55 11 91234-0010: the SMS the application sent with the code chip 482913, and the reply the handset sent.

    The empty state offers Nova mensagem de teste (new test message: it goes through the API, the way your application would send it), Carregar exemplos (load the samples) and Abrir painel de controle (open the control panel, with the delivery, webhook, event, magic-number and API tabs). To seed the sample conversations from the command line:

    curl -X POST http://localhost:8899/_mock/sms/demo
  3. Send an SMS

    Send an outbound SMS using the native REST API to +5511912340010. The mock parses the message body, detects GSM-7 or UCS-2 encoding, calculates segments, extracts OTP codes, and places the message in queued status:

    curl -X POST http://localhost:8899/v1/messages \
      -H 'Content-Type: application/json' \
      -d '{
        "to": "+5511912340010",
        "body": "Your authentication code is 849201",
        "from": "AUTH"
      }'
    {
      "id": "SM9f2c4e1b7a3d48c5b06e1d2f93a7c8b4",
      "direction": "outbound",
      "from": "AUTH",
      "to": "+5511912340010",
      "body": "Your authentication code is 849201",
      "status": "queued",
      "api": "native",
      "encoding": "GSM-7",
      "segments": 1,
      "codes": [
        "849201"
      ],
      "errorCode": null,
      "errorMessage": null,
      "callbackUrl": null,
      "createdAt": "2026-10-29T21:00:00.000Z",
      "updatedAt": "2026-10-29T21:00:00.000Z",
      "sentAt": null,
      "deliveredAt": null,
      "history": [
        {
          "status": "queued",
          "at": "2026-10-29T21:00:00.000Z"
        }
      ]
    }

    By default (SMS_AUTO_DELIVER=1), the message automatically transitions to sent after delay/3, then to delivered after SMS_DELIVERY_DELAY_MS (default 1500 ms).

  4. Read it back

    Query messages for a given phone number. In automated tests, pass wait=5000 to wait up to 5 seconds for a match: it answers at once when one exists; add status=delivered to wait for the delivery:

    curl 'http://localhost:8899/v1/messages?to=%2B5511912340010&wait=5000'
    {
      "messages": [
        {
          "id": "SM9f2c4e1b7a3d48c5b06e1d2f93a7c8b4",
          "direction": "outbound",
          "from": "AUTH",
          "to": "+5511912340010",
          "body": "Your authentication code is 849201",
          "status": "delivered",
          "api": "native",
          "encoding": "GSM-7",
          "segments": 1,
          "codes": [
            "849201"
          ],
          "errorCode": null,
          "errorMessage": null,
          "callbackUrl": null,
          "createdAt": "2026-10-29T21:00:00.000Z",
          "updatedAt": "2026-10-29T21:00:01.500Z",
          "sentAt": "2026-10-29T21:00:00.500Z",
          "deliveredAt": "2026-10-29T21:00:01.500Z",
          "history": [
            {
              "status": "queued",
              "at": "2026-10-29T21:00:00.000Z"
            },
            {
              "status": "sent",
              "at": "2026-10-29T21:00:00.500Z"
            },
            {
              "status": "delivered",
              "at": "2026-10-29T21:00:01.500Z"
            }
          ]
        }
      ],
      "count": 1
    }
  5. Reply as handset

    Simulate the handset user replying to your outbound SMS. Inbound messages have direction: "inbound" and status: "received", update the conversation thread in the panel in real time, and dispatch an inbound webhook if configured:

    curl -X POST http://localhost:8899/_mock/sms/inbound \
      -H 'Content-Type: application/json' \
      -d '{
        "from": "+5511912340010",
        "body": "CONFIRMAR"
      }'
    {
      "id": "SM1d7e5a0c93b248f6a8e4c2b7f06d31a9",
      "direction": "inbound",
      "from": "+5511912340010",
      "to": "AMA4DEVS",
      "body": "CONFIRMAR",
      "status": "received",
      "api": "mock",
      "encoding": "GSM-7",
      "segments": 1,
      "codes": [],
      "errorCode": null,
      "errorMessage": null,
      "callbackUrl": null,
      "createdAt": "2026-10-29T21:00:05.000Z",
      "updatedAt": "2026-10-29T21:00:05.000Z",
      "sentAt": null,
      "deliveredAt": null,
      "history": [
        {
          "status": "received",
          "at": "2026-10-29T21:00:05.000Z"
        }
      ]
    }
  6. Trigger a failure

    Test error handling deterministically by sending to numbers with specific endings. Sending to a number ending in 0001 produces a delivery failure with error code 30003:

    curl -X POST http://localhost:8899/v1/messages \
      -H 'Content-Type: application/json' \
      -d '{
        "to": "+5511912340001",
        "body": "Delivery failure simulation"
      }'

    Magic number suffixes supported:

    • 0001 — Delivery failure (transitions to failed with error 30003)
    • 0002 — Undelivered handset (transitions to undelivered with error 30005)
    • 0003 — Immediate rejection at submission (HTTP 400 rejected_number / Twilio code 21211)
    • 0004 — Stuck indefinitely in sent status
    • 0005 — Filtered by carrier spam protections (status failed with error 30007)
  7. Point Twilio client

    Integrate with existing Twilio applications without changing SDK calls. Point your Twilio client to http://localhost:8899 or submit standard form requests:

    curl -X POST http://localhost:8899/2010-04-01/Accounts/ACmock/Messages.json \
      -u ACmock:mocktoken \
      -d "To=+5511912340010" \
      -d "From=AMA4DEVS" \
      -d "Body=Testing Twilio compatibility"
    {
      "sid": "SM6b0a8f3e2c7d41958e1f4a2d9c3b7e05",
      "account_sid": "ACmock",
      "api_version": "2010-04-01",
      "body": "Testing Twilio compatibility",
      "date_created": "Thu, 29 Oct 2026 21:00:00 +0000",
      "date_updated": "Thu, 29 Oct 2026 21:00:00 +0000",
      "date_sent": null,
      "direction": "outbound-api",
      "error_code": null,
      "error_message": null,
      "from": "AMA4DEVS",
      "to": "+5511912340010",
      "num_segments": "1",
      "num_media": "0",
      "price": null,
      "price_unit": "USD",
      "status": "queued",
      "messaging_service_sid": null,
      "uri": "/2010-04-01/Accounts/ACmock/Messages/SM6b0a8f3e2c7d41958e1f4a2d9c3b7e05.json",
      "subresource_uris": {
        "media": "/2010-04-01/Accounts/ACmock/Messages/SM6b0a8f3e2c7d41958e1f4a2d9c3b7e05/Media.json"
      }
    }
  8. Turn it off

    The SMS mock stores all messages, conversations, and webhook logs in memory. Stop the container with Ctrl+C or docker stop; restarting provides a completely clean slate.