MySMSGate turns an Android phone into your own SMS gateway. Messages are sent over the REST API
below and delivered by the SIM card in your phone, so there is no per-country carrier pricing and
no per-message charge at all — you pay $10.00/month for the first phone and $5.00/month
for each additional one, whatever you send. A multi-part message up to 7 segments
(1071 GSM-7 characters / 469 Unicode characters) counts as one message.
Examples below use YOUR_API_KEY as a placeholder.
Create a free account to get a real key —
the same page then shows these examples with your key already filled in.
Authentication
All API requests require a Bearer token in the Authorization header:
SMS is queued and delivered to the phone in batches according to the device SMS/min rate limit. If the phone is offline, a push notification is sent to wake it up.
in for received messages, out for sent ones. Omit for both. Any other value returns 400.
status
string
—
pending, sending, sent or failed. Any other value returns 400.
since_id
int
—
Return only messages newer than this id, oldest first. Use it to poll for new messages instead of paging with offset.
Polling for new messages
To watch for incoming SMS, poll with direction=in and the since_id you got back last time. Start with since_id=0 to read from the beginning. Messages come oldest first, and the response carries next_since_id — store it and pass it on the next call. An empty page returns the same next_since_id you sent, so a quiet minute never rewinds your cursor.
Do not page through new messages with offset: new rows land at the top of the default ordering, so the window shifts under you and you would both skip and repeat messages. since_id is a cursor and does not move.
Get an HTTP POST the moment something happens, instead of polling /api/v1/history.
Events
Event
Fires when
message.received
One of your phones receives an SMS
message.status
An outgoing message reaches a final state: sent or failed
Importing a phone's existing messages (/api/v1/inbox/sync) does not fire message.received — otherwise connecting a phone for the first time would replay years of old texts into your endpoint.
{
"webhook": {
"id": "0f3c...",
"url": "https://example.com/sms-hook",
"secret": "whsec_...",
"events": ["message.received", "message.status"],
"format": "json",
"is_active": true,
"created_at": "2026-09-08T16:20:00Z"
},
"note": "Store the secret now: it is shown only in this response."
}
The signing secret appears in this response and never again — listings omit it. Lost it? Delete the webhook and register it again. GET /api/v1/webhooks lists your endpoints, DELETE /api/v1/webhooks?id=... removes one. Up to 5 per account.
phone_to is usually empty on received messages, and that is not a fault: Android cannot read the SIM's own number on most carriers, so the device — device_id — is the addressable endpoint here, not a number. A message.status event carries status and, when it failed, error.
Verifying the signature
HMAC-SHA256 over <timestamp>.<raw body> with your webhook secret. The timestamp is inside the signed string, so a captured request cannot be replayed later — reject anything older than five minutes. HTTP header names are case-insensitive: read the header through your framework’s accessor (request.headers[...]) rather than a raw dictionary lookup, and it will not matter how the casing reaches you.
import hmac, hashlib, time
def verify(secret, header, raw_body, tolerance=300):
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts["t"], parts["v1"]
if abs(time.time() - int(ts)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Sign the raw body, before any JSON parsing — re-serialising it changes the bytes and the signature will not match.
Delivery, retries and failure
Any 2xx counts as delivered. Answer quickly and do the work afterwards — we wait 15 seconds.
Anything else is retried 5 times over about two and a half hours (30s, 2m, 10m, 30m, 2h).
410 Gone is taken at face value: no retries, we treat the endpoint as retired.
After 5 deliveries fail their whole retry schedule in a row, the webhook is switched off and we email you. Events are not replayed afterwards — catch up with GET /api/v1/history?direction=in&since_id=....
Redirects are not followed, and the endpoint must be reachable on the public internet: private, loopback and link-local addresses are refused, both when you register and on every delivery.
The same event is delivered once. A message that fails and is then accepted late produces two message.status events — the correction is not a duplicate.
Already using Twilio? Change one line
We answer Twilio’s own REST shape at /2010-04-01/Accounts/{AccountSid}/Messages, with Twilio’s parameter names, response fields and error bodies. Point the official SDK at us and the rest of your code is untouched.
from twilio.rest import Client
client = Client(ACCOUNT_SID, AUTH_TOKEN)
client.api.base_url = "https://mysmsgate.net" # the only line you add
msg = client.messages.create(to="+14155551234", body="Hello")
print(msg.sid, msg.status)
Your credentials
Twilio field
What to put there
Account SID
ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Auth Token
your API key, above — with or without the dashes
From
the id of the phone that should send, from GET /api/v1/devices. Omit it if the account has one phone. The SIM’s own number works too when the phone could read it — most cannot.
What is different, and we would rather say it plainly
There is no delivered status. Twilio learns delivery from carrier receipts; your messages leave your own SIM and nobody reports back. You will see queued, then sent (the phone handed it to the network) or failed. We will not print delivered just to look familiar.
From is a phone, not a number. Replies come back to that handset and appear in your account, so the sender your recipient sees is your own SIM — not a number we rent you.
price is always null. There is no per-message price to report: you pay per connected phone.
Unsupported parameters are rejected, not ignored.MediaUrl (no MMS) and StatusCallback (use the account-level webhook above instead) return a 400 that says so. Silently dropping them would look like a working migration and behave like a broken one.