Custom SMS Gateway Implementation
This page explains how to connect an SMS carrier that Thirdlane does not support out of the box by using the Custom gateway provider. It covers the Configuration Manager fields, the webhooks the platform exposes, the request/response contracts your adapter must speak, and an optional reference adapter (a small local HTTP service that bridges Twilio to the Custom-provider contract) that you can download and run for lab testing.
If you just need to configure a standard Telnyx/Twilio/Bandwidth gateway, see Messaging Gateways.
What the Custom provider gives you
Selecting Provider = Custom on an SMS gateway tells Configuration Manager to use its generic SMS contract instead of a built-in carrier driver. Once saved, the platform exposes:
- Outbound sending — Configuration Manager submits outbound messages to the URL you configure in Provider’s URL. Your adapter or your carrier’s REST endpoint is responsible for actually placing the message with the carrier.
- Inbound webhook —
https://<domain>/sms/<gateway-name>/genericon the selected gateway domain. Your adapter (or the carrier itself, if it can be configured to post directly) forwards received SMS/MMS events here. - Delivery status webhook —
https://<domain>/sms/<gateway-name>/generic/statusfor delivery receipts.
Replace <domain> with the value picked in the gateway’s Domain field and <gateway-name> with the gateway’s Name.
Configuration Manager fields
Open Configuration Manager -> Communications Settings -> Messaging Gateways and create a new gateway.
- Name — unique alphanumeric name (no spaces). Appears in the generated webhook URLs.
- Provider — set to Custom.
- Description — optional.
- Domain — public domain that Twilio / your carrier / your adapter will reach inbound and status webhooks on.
- Provider’s URL — full URL to which outbound send requests will be submitted. Point this at your own adapter, your carrier’s API, or (for lab testing) at the bundled reference sample gateway.
- Account SID — identifier passed in outbound requests, reused as a generic credential.
- Auth Token — secret paired with Account SID.
After the gateway is saved, the form shows the generated Webhook URL and Delivery Status URL. Configure those at your carrier’s portal (or, for the reference adapter, at Twilio).
You still need to flag the relevant DIDs as SMS-enabled and configure Inbound Messaging Routes so that inbound messages reach users or queues.
Message flow
flowchart LR App["Your app / Configuration Manager"] -->|"outbound send"| Adapter["Your adapter or carrier API<br/>(Provider's URL)"] Adapter -->|"carrier API call"| Carrier["SMS carrier"] Carrier -->|"inbound SMS"| Adapter Carrier -->|"delivery receipt"| Adapter Adapter -->|"POST /sms/<name>/generic"| Platform["Thirdlane Platform"] Adapter -->|"POST /sms/<name>/generic/status"| PlatformYour adapter is the translation layer: it speaks the carrier’s API on one side and the Custom-provider contract on the other. Some carriers can be configured to POST directly to the platform webhooks, in which case the adapter is only needed for outbound translation (or not at all, if the carrier’s outbound API is compatible with what Configuration Manager submits).
Request/response contracts
The platform uses a Twilio-compatible form-encoded shape for outbound, inbound, and status events.
1. Outbound send
Configuration Manager submits a POST to the configured Provider’s URL with:
- Content-Type:
application/x-www-form-urlencoded - Fields:
From— sender number in E.164To— recipient number in E.164Body— message textMediaUrl— (optional) media URL for MMSStatusCallback— the generated Delivery Status URL for the gateway
Your endpoint should respond with JSON compatible with Twilio’s Messages.json response (at minimum a message sid and an initial status such as queued).
2. Inbound webhook
Your adapter (or the carrier) posts inbound events to:
POST https://<domain>/sms/<gateway-name>/genericContent-Type: application/x-www-form-urlencodedTypical fields:
MessageSidFromToBodyNumMedia- Media fields when
NumMedia > 0:MediaUrl0,MediaContentType0, …
3. Delivery status webhook
Delivery updates post to:
POST https://<domain>/sms/<gateway-name>/generic/statusContent-Type: application/x-www-form-urlencodedTypical fields:
MessageSidSmsSidMessageStatus/SmsStatus(queued,sent,delivered,undelivered,failed,received)FromToRawDlrDoneDate(optional, carrier-specific)
Reference sample gateway (Twilio adapter)
A minimal reference adapter is provided as a starting point. It runs as a small local HTTP service that:
- Sends outbound SMS through the Twilio API.
- Receives inbound SMS/MMS from Twilio and forwards the payload to your upstream Custom-provider inbound URL.
- Forwards delivery status callbacks from Twilio to your upstream status URL.
Three equivalent implementations are provided; pick whichever language your operators are comfortable with:
- sms_gw.pl — Perl.
- sms_gw.py — Python 3, standard library only (no
pippackages). - sms_gw.js — Node.js, built-in modules only (no
npm install).
Only one sample can listen on the default port (65533) at a time.
Configuration constants
Edit the constants at the top of the sample you choose:
SID— Twilio Account SID.TOKEN— Twilio Auth Token.DOMAIN_API_SMS_SERVICE— your upstream host (the Domain set on the Custom gateway).URI_API_SMS_SERVICE_INBOUND— upstream path for inbound events, e.g./sms/<gateway-name>/generic.URI_API_SMS_SERVICE_STATUS— upstream path for delivery status events, e.g./sms/<gateway-name>/generic/status.
Local listener defaults:
- Address:
127.0.0.1 - Port:
65533
Local routes served by the sample
These routes are sample-only and intentionally prefixed with /testsms/ so they do not collide with the platform’s production /sms/... webhooks:
GET/POST /testsms/generic/send— sends an SMS using form fieldsfrom,to,body, optionalmediaUrl.POST /testsms/generic— receives inbound Twilio payload and forwards it to your upstream inbound URL.POST /testsms/generic/status— receives Twilio status callbacks and forwards them to your upstream status URL.
Example outbound call:
curl -X POST "http://127.0.0.1:65533/testsms/generic/send" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "from=+14159917673" \ --data-urlencode "to=+15555550123" \ --data-urlencode "body=Hello from Custom provider"Run
From the directory where you saved the sample, run one of:
perl sms_gw.plpython3 sms_gw.pynode sms_gw.jsExpected startup output:
Upstream: http://127.0.0.1:65533/Exposing the sample via Nginx
If Twilio (or Configuration Manager) must reach the sample over HTTPS on your server, add the bundled Nginx snippets.
Upstream — save as /etc/nginx/conf.d/https/upstream/custom/custom_sms_gw.conf:
- Download: custom_sms_gw.conf
upstream custom_sms_gw { server 127.0.0.1:65533;}Service location — save as /etc/nginx/conf.d/https/service/custom/custom_sms_gw_svc.conf:
- Download: custom_sms_gw_svc.conf
location /testsms { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://custom_sms_gw; proxy_read_timeout 86400s; proxy_http_version 1.1;}Then reload Nginx:
sudo nginx -t && sudo systemctl reload nginxMoving toward production
The reference adapter is a teaching tool. Before relying on a Custom SMS gateway in production, review each of these with your operations and security teams.
1. Define a canonical event model
Normalize messages into a stable internal shape independent of the carrier: message_id, provider_message_id, direction, from, to, text, media[], status, provider_raw_payload, received_at.
2. Separate concerns
Split the adapter into clear layers: transport (HTTP server/client), provider adapters (carrier-specific send and webhook mapping), business logic (validation, normalization, persistence, retries), and observability (structured logs, trace IDs, metrics).
3. Validate input strictly
Enforce E.164 phone format, message length policy, allowed media MIME types; reject malformed callbacks with HTTP 4xx.
4. Verify webhook authenticity
Validate carrier signatures (for Twilio, verify X-Twilio-Signature). Reject unsigned or invalid callbacks. Enforce HTTPS and, where available, timestamp replay protection.
5. Add reliability guarantees
Retry transient failures (429/5xx, timeouts) with exponential backoff. Use an idempotency key per message SID. Persist delivery attempts and outcomes. Deduplicate repeated callbacks.
6. Build a full status state machine
Normalize statuses across carriers (queued, sent, delivered, undelivered, failed, received) and reject invalid transitions (for example, delivered -> queued).
7. Design for multi-provider
Abstract provider-specific code behind send(provider, payload), parse_inbound(provider, raw_event), parse_status(provider, raw_event) to make switching carriers low-risk.
8. Harden security
Move secrets to environment variables or a secret manager. Enable TLS certificate verification. Authenticate internal send endpoints. Add rate limiting and request size limits. Sanitize logs to avoid leaking PII and secrets.
9. Add observability and alerting
Track outbound send success/failure, callback processing latency, carrier API latency and error rates, and the delivery funnel. Alert on sudden drops in deliveries, spikes in 401/403/429/5xx from the carrier, and callback endpoint failures.
10. Build a test matrix
At minimum: valid outbound SMS, MMS with media URL, invalid destination, carrier timeout/retry path, inbound SMS without media, inbound MMS with multiple media items, status transitions through all terminal states, and duplicate callback delivery.