twilio.twins.la

A digital twin of the Twilio SMS and SendGrid Email APIs.

What is this?

This is a high-fidelity digital twin of Twilio's SMS API and SendGrid's Email API. Code you write against this twin will work against the real Twilio and SendGrid with only hostname and credential changes. No Twilio account needed to develop.

Supported scenarios

How to use it

Cloud: Point your app at https://twilio.twins.la instead of api.twilio.com. Create an account via POST /_twin/accounts and use the returned credentials.

Local: Install with pip install twins-twilio-local and run a local instance on any port. Same API, same behavior, your machine.

For agents

Copy this into your agent's system prompt, tool configuration, or CLAUDE.md. Also available as plain text at /_twin/agent-instructions.

# Twilio & SendGrid Twin — twilio.twins.la

A high-fidelity digital twin of the Twilio SMS and SendGrid Email APIs.
Code written against this twin works against the real Twilio/SendGrid
with only hostname and credential changes.

## Authentication

SMS/Voice API: HTTP Basic Auth
  Username: Account SID (starts with AC)
  Password: Auth Token

Email API: Bearer token
  Header: Authorization: Bearer SG.{key_id}.{key_secret}

Twin Plane: HTTP Basic Auth (same credentials)
  Most Twin Plane operations use the same AccountSid:AuthToken.
  Exception: POST /_twin/accounts requires no auth (creates credentials).

Twin Plane Admin: Bearer token
  Header: Authorization: Bearer <admin_token>
  Service-wide operations (list all accounts, review feedback, etc.)
  require an admin token set by the deployment owner.

Create credentials:
  POST /_twin/accounts          — no auth required, returns { sid, auth_token }

## Key Endpoints

Twin Plane (no auth):
  GET  /_twin/health             — status check
  GET  /_twin/scenarios          — supported scenarios
  GET  /_twin/settings           — twin settings
  GET  /_twin/references         — authoritative sources used to build this twin
  POST /_twin/accounts           — create account

Twin Plane (Basic Auth — use AccountSid:AuthToken):
  GET  /_twin/accounts           — get your account details
  GET  /_twin/logs               — your operation logs
  POST /_twin/api-keys           — create SendGrid API key
  GET  /_twin/emails             — list your emails
  POST /_twin/simulate/inbound   — drive a signed inbound SMS or MMS
                                   to the phone number's SmsUrl. Body:
                                   { account_sid, from, to, body,
                                     num_segments?, num_media?,
                                     media_urls?, media_content_types? }.
                                   STOP/START/HELP carrier keywords are
                                   auto-detected and applied to opt-out
                                   state; HELP triggers a canned auto-reply.
  POST /_twin/simulate/status    — force a status transition on a
                                   tenant-owned outbound message and
                                   fire its StatusCallback. Body:
                                   { message_sid, status, error_code?,
                                     error_message? }. Status is one of
                                   queued | sending | sent | delivered |
                                   failed | undelivered. error_code is
                                   required for failed and undelivered.
  GET  /_twin/media/<media_sid>  — placeholder PNG for MMS scenarios
                                   (twin-served, unauthenticated)
  POST /_twin/feedback           — submit feedback
  GET  /_twin/feedback           — list your feedback

Twin Plane (Admin Bearer — service-wide access):
  GET  /_twin/accounts           — list all accounts
  GET  /_twin/logs               — all operation logs
  GET  /_twin/emails             — all emails
  GET  /_twin/feedback           — all feedback
  POST /_twin/feedback/<id>      — update any feedback (for review pipeline)

Twilio SMS API (Basic Auth):
  POST /2010-04-01/Accounts/{AccountSid}/Messages.json         — send SMS
  GET  /2010-04-01/Accounts/{AccountSid}/Messages.json         — list messages
  GET  /2010-04-01/Accounts/{AccountSid}/Messages/{Sid}.json   — get message
  POST /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers.json — provision number
  GET  /2010-04-01/Accounts/{AccountSid}/IncomingPhoneNumbers.json — list numbers

SendGrid Email API (Bearer Auth):
  POST /v3/mail/send             — send email (returns 202, X-Message-Id header)

## Quick Start

1. Create an account (no auth needed):
   curl -X POST https://twilio.twins.la/_twin/accounts \
     -H "Content-Type: application/json" \
     -d '{"friendly_name": "My App"}'

2. Send an SMS (use sid and auth_token from step 1):
   curl -X POST https://twilio.twins.la/2010-04-01/Accounts/{sid}/Messages.json \
     -u "{sid}:{auth_token}" \
     -d "To=+15551234567" -d "From=+15559876543" -d "Body=Hello from twin"

3. Check logs (same credentials work for Twin Plane):
   curl https://twilio.twins.la/_twin/logs \
     -u "{sid}:{auth_token}"

4. Drive a signed inbound SMS against your registered SmsUrl (CI/smoke):
   curl -X POST https://twilio.twins.la/_twin/simulate/inbound \
     -u "{sid}:{auth_token}" \
     -H "Content-Type: application/json" \
     -d '{"account_sid":"{sid}","from":"+15559876543","to":"+15551112222","body":"hi"}'

   The twin POSTs urlencoded form data to your SmsUrl with a valid
   X-Twilio-Signature header computed against the *exact* registered URL.
   Consumers that rebuild the request URL incorrectly behind a proxy
   (e.g., ignoring X-Forwarded-Proto) will fail signature validation —
   that is the bug class this control endpoint is designed to surface.

5. Force a delivery failure status and fire StatusCallback:
   curl -X POST https://twilio.twins.la/_twin/simulate/status \
     -u "{sid}:{auth_token}" \
     -H "Content-Type: application/json" \
     -d '{"message_sid":"SM...","status":"failed","error_code":30003}'

## Local Usage

pip install twins-twilio-local
python -c "
from twins_twilio_local.storage_sqlite import SQLiteStorage
from twins_twilio.app import create_app
storage = SQLiteStorage('twin.db')
app = create_app(storage=storage)
app.run(port=8080)
"

Then use http://localhost:8080 instead of https://twilio.twins.la.

## Feedback

We actively want your feedback. If you encounter a limitation, a missing
scenario, unexpected behavior, or have a feature request — please tell us.
Your feedback directly shapes what we build next.

Submit feedback (requires Basic Auth):
  curl -X POST https://twilio.twins.la/_twin/feedback \
    -u "{sid}:{auth_token}" \
    -H "Content-Type: application/json" \
    -d '{
      "body": "Description of what you encountered",
      "category": "bug",
      "context": {"message_sid": "SM...", "error": "..."}
    }'

Required field:
  body    — Describe what happened, what you expected, and what you need.

Optional fields:
  category    — One of: "bug", "missing-scenario", "feature-request", "general"
  context     — Any structured data that helps us understand the issue:
                error codes, message SIDs, request/response snippets,
                scenario names, or steps to reproduce.

Tips for useful feedback:
  - Include IDs (message SIDs, error codes) when reporting failures.
  - Describe the scenario you were trying to accomplish, not just the error.
  - If a feature is missing, describe your use case — what you'd build with it.
  - "I tried X, expected Y, got Z" is the most useful format for bugs.

Don't hold back. Even if you're unsure whether something is a bug or a
limitation, submit it. Feedback about confusing documentation, awkward APIs,
or missing examples is just as valuable as bug reports.

## Reference

Detailed docs: https://github.com/twins-la/twilio
Project overview: https://twins.la
All twins: https://github.com/twins-la/twins-la