SMS and MMS

Connect Twilio, Amazon SNS, AWS End User Messaging, or Azure Communication Services to the local text emulator.

What Sqrzl does

Sqrzl captures outbound text requests, stores them for inspection, and lets you simulate inbound messages and delivery updates. It does not send messages through a carrier. Start Sqrzl with the quickstart; SMS APIs use http://localhost:9000 and the admin UI uses http://localhost:9001.

Provider endpoints

ProviderLocal entry pointSupported send operation
Twiliohttp://localhost:9000POST /2010-04-01/Accounts/{AccountSid}/Messages.json
Amazon SNShttp://localhost:9000Query API Publish with a direct phone number.
AWS End User Messaging SMS Voice v2http://localhost:9000SendTextMessage and SendMediaMessage.
Azure Communication Services SMShttp://localhost:9000POST /sms?api-version=... with one or more recipients.

Connect an SDK

Twilio

import os
import twilio

client = twilio.Client(
    os.environ["SQRZL_TWILIO_ACCOUNT_SID"],
    os.environ["SQRZL_TWILIO_AUTH_TOKEN"],
)
client.api.base_url = "http://localhost:9000"
message = client.messages.create(
    to="+15550001001",
    from_="+15550001000",
    body="Local Sqrzl test",
)

Configure both SQRZL_TWILIO_ACCOUNT_SID and SQRZL_TWILIO_AUTH_TOKEN on Sqrzl to require Basic authentication and sign Twilio callbacks.

Amazon SNS

import boto3

client = boto3.client(
    "sns",
    endpoint_url="http://localhost:9000",
    aws_access_key_id="admin",
    aws_secret_access_key="sqrzl-secret",
    region_name="us-east-1",
)
client.publish(
    PhoneNumber="+15550001002",
    Message="Local Sqrzl test",
)

AWS End User Messaging SMS Voice v2

import boto3

client = boto3.client(
    "pinpoint-sms-voice-v2",
    endpoint_url="http://localhost:9000",
    aws_access_key_id="admin",
    aws_secret_access_key="sqrzl-secret",
    region_name="us-east-1",
)
client.send_text_message(
    DestinationPhoneNumber="+15550001003",
    OriginationIdentity="+15550001000",
    MessageBody="Local Sqrzl test",
    MessageType="TRANSACTIONAL",
)

The v2 adapter also supports send_media_message with an optional S3 media URI, plus DryRun validation without capture.

Azure Communication Services SMS

import base64
from azure.communication.sms import SmsClient
from azure.core.pipeline.transport import RequestsTransport

class LocalHttpTransport(RequestsTransport):
    def send(self, request, **kwargs):
        if request.url.startswith("https://"):
            request.url = "http://" + request.url.removeprefix("https://")
        return super().send(request, **kwargs)

access_key = base64.b64encode(b"shared-secret").decode("ascii")
connection = (
    "endpoint=http://localhost:9000;accesskey=" + access_key
)
client = SmsClient.from_connection_string(
    connection,
    transport=LocalHttpTransport(),
)
client.send(
    from_="+15550001000",
    to=["+15550001004", "+15550001005"],
    message="Local Sqrzl test",
    enable_delivery_report=True,
)

The ACS SDK normalizes its endpoint to HTTPS. This transport rewrites the signed request to the emulator's local HTTP listener. Set SQRZL_ACS_CONNECTION_STRING on Sqrzl with the same access key.

Configure authentication

  • Twilio uses SQRZL_TWILIO_ACCOUNT_SID and SQRZL_TWILIO_AUTH_TOKEN together.
  • AWS adapters use SQRZL_ACCESS_KEY_ID and SQRZL_SECRET_ACCESS_KEY when generic API authentication is enabled.
  • ACS SMS uses the shared key in SQRZL_ACS_CONNECTION_STRING.

The checked-in Compose file sets generic credentials to admin and sqrzl-secret. See configuration to change them.

Inspect and simulate messages

Open http://localhost:9001 and choose Texts. The admin surface lets you search messages and conversations, inject an inbound message, change an outbound message to delivered or failed, configure callback destinations by provider and local number, inspect callback requests and their bounded responses, and create linked retries.

Delivery changes are explicit. Sqrzl does not advance messages on a timer. Callback failures do not undo a captured message or delivery state; redirects are not followed.

Callback access

Callback destinations may use localhost, 127.0.0.1, or ::1 by default. Add other local callback hosts as a comma-separated list in SQRZL_TEXT_CALLBACK_ALLOWED_HOSTS. Each callback attempt times out after 5000 ms by default; set SQRZL_TEXT_CALLBACK_TIMEOUT_MS to change the limit.

A Twilio StatusCallback URL must pass the same host allowlist. Outbound media URLs are references only: Sqrzl does not fetch them. Inbound Twilio simulations can include base64 media that is stored and served from authenticated Twilio-shaped media URLs.

Supported scope

  • Twilio accepts To, From or MessagingServiceSid, an optional body up to 1600 characters, up to 10 absolute HTTP(S) media URLs, and an optional allowlisted status callback. A service-selected sender initially returns accepted with zero segments; an explicit sender returns queued.
  • SNS accepts direct E.164 PhoneNumber Publish requests, raw messages or MessageStructure=json selecting the sms value with default fallback, and up to 10 message attributes.
  • SMS Voice v2 accepts the documented JSON fields for its text and media send operations. MMS can reference one optional S3 media URI; remote media is never fetched.
  • ACS SMS supports API versions 2021-03-07 and 2026-01-23, 1 to 100 E.164 recipients, per-recipient repeatability results, delivery-report options, tags, and timeouts from 60 to 43200 seconds. Rejected recipients in a mixed result are not captured.

SNS topics and fan-out, AWS inbound MMS, ACS MMS, TwiML execution, WhatsApp, RCS, voice calls, automatic delivery timing, remote media fetching, and provider management APIs are unsupported. Check the compatibility matrix for exact request fields and errors.

Request and capture limits

SQRZL_MAX_REQUEST_BYTES defaults to 128 MiB per HTTP request body. Separately, each capture transaction has a conservative 64 MiB projected aggregate materialization limit, including message fan-out copies and inline media.

Oversized requests receive a provider-shaped 413 and create no partial captures. This capture limit is separate from the request-body setting; see configuration for that setting.