Verify
Phone verification in two calls. SimpleSMS generates the code, sends it, enforces expiry and attempt limits, and defends against SMS pumping. You never store a code, and you don't need to own a phone number.
The whole integration
Two calls, no dependency required:
const send = (to) => fetch('https://api.joinsimplesms.com/v1/verify', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SIMPLESMS_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ to }),
}).then((r) => r.json());
const check = (to, code) => fetch('https://api.joinsimplesms.com/v1/verify/check', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.SIMPLESMS_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ to, code }),
}).then((r) => r.json());Or from the terminal: the CLI ships inside the SDK package, so npx needs
no install at all:
SIMPLESMS_API_KEY=ssms_sk_test_... npx joinsimplesms verify +14155550132
npx joinsimplesms verify +14155550132 482193 # check the codeOr with the official SDK, which adds retries, typed errors and automatic idempotency keys:
npm install joinsimplesmsimport { SimpleSMS } from 'joinsimplesms';
const sms = new SimpleSMS(process.env.SIMPLESMS_API_KEY);
await sms.verify.send({ to: '+14155550132' });
const { verified } = await sms.verify.check({ to: '+14155550132', code });In Python (pip install joinsimplesms, no dependencies):
from joinsimplesms import SimpleSMS
client = SimpleSMS() # reads SIMPLESMS_API_KEY
client.verify.send(to="+14155550132")
verified = client.verify.check(to="+14155550132", code=code)["verified"]Send a code
curl -X POST https://api.joinsimplesms.com/v1/verify \
-H "Authorization: Bearer ssms_sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "+14155550132" }'{
"id": "ver_a1B2c3D4e5F6g7H8",
"object": "verification",
"phone": "+14155550132",
"status": "pending",
"attempts": 0,
"charged": false,
"expires_at": "2026-08-07T12:10:00.000Z"
}Send an Idempotency-Key header so that a retried request, after a timeout
for example, returns the first verification instead of texting a second code.
It works the same way as on messages.
Optional app_name (24 chars max) puts your product's name in the message.
The body is a fixed SimpleSMS template; you can't set the text, which is what
keeps verification traffic out of spam filtering.
You don't need a phone number
Verify sends from SimpleSMS's own verification numbers, registered under our 10DLC campaign. You don't buy a number, you don't provision anything, and you pay no monthly number fee; verification is the whole product.
The same recipient always gets codes from the same sender, so a second code lands in the thread they already have.
If you'd rather codes came from a number you own, pass it as from:
{ "phone": "+14155550132", "from": "+16155550184" }Platforms verifying for their own customers can pass customer_id; it is
echoed on the verification and its events and counted in that
customer's usage.
Check the code
curl -X POST https://api.joinsimplesms.com/v1/verify/check \
-H "Authorization: Bearer ssms_sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "+14155550132", "code": "482193" }'{ "verified": true, "status": "approved", "attempts": 1, "charged": true }That's the whole integration.
You only pay when it works
A verification is billed only when a code is actually verified. Wrong
codes, expiries, abandoned flows and anything Shield blocks are free;
every response tells you plainly with charged.
Rules SimpleSMS enforces for you
| Code lifetime | 10 minutes |
| Check attempts | 5, then the code is dead |
| Resend cooldown | 60 seconds per number |
| Per number | 5 an hour, 10 a day |
Statuses: pending, approved, expired, max_attempts, blocked.
Shield
SMS pumping is fraud where someone farms revenue-share by triggering verification codes to numbers they control. SimpleSMS blocks it before you're charged:
- US and Canada only. Country code +1 also covers Jamaica, the Dominican Republic and the Bahamas, the classic pumping destinations. We allowlist real US and Canadian area codes and reject the rest.
- Velocity limits per destination, per account and per source.
- No VoIP. Disposable VoIP numbers are rejected.
Blocked attempts return 403 with code verification_blocked, a reason,
and charged: false.
Sandbox
Test keys never send a real message. Any code you send returns a verification
whose code is 111111, and these codes are deterministic:
| Code | Result |
|---|---|
111111 | approved |
000000 | invalid |
222222 | expired |
333333 | max attempts |
Sandbox has no resend cooldown, so you can iterate on a "resend code"
button freely. Live enforces 60 seconds per number. To test that path
deterministically, send to +15005550003; it always returns the cooldown
429 with retry_after in the body.
Retrieve a verification
GET /v1/verify/{id} returns the object with its current status, attempt
count, and whether it was charged.