One-time codes
Generate, send and verify a one-time code without storing or handling the code yourself.
The code is generated here, sent through the normal send pipeline (so it is billed and tracked like any other message), and stored only as an HMAC — it is never returned to you and never readable in our database.
Send a code
POST
/api/v1/otp/sendParameters
sender
stringrequired
An approved sender ID.
phone
stringrequired
The number to verify.
message
string
Template containing
{{code}}, and optionally {{expiry}}. Defaults to a standard message. A template without {{code}} is rejected.length
integer
Digits in the code, 4–10. Defaults to 6.
expiry_minutes
integer
1–30. Defaults to 5.
max_attempts
integer
Wrong guesses before the code is burned, 1–10. Defaults to 5.
medium
string
sms (default) or voice. With voice we call and read the code aloud, billed from your voice credit — see Voice OTP.Shell
curl -X POST https://api.jkasms.com/api/v1/otp/send \
-H "Authorization: Bearer $JKASMS_SECRET_KEY" \
-H "X-JKASMS-Org-Id: $JKASMS_ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"sender": "JKASMS",
"phone": "0241234567",
"message": "Your code is {{code}}. It expires in {{expiry}} minutes.",
"expiry_minutes": 5
}'Response202
JSON
{
"success": true,
"data": {
"request_id": "cmu9x2k4a0001abcd",
"phone": "+233241234567",
"expires_at": "2026-09-21T10:35:00.000Z",
"segments": 1,
"cost": "0.06",
"currency": "GHS"
}
}The code itself is deliberately absent from the response. It exists only in the SMS and as an HMAC in our database — so a leak of your API logs does not leak your users' codes.
Verify a code
POST
/api/v1/otp/verifyParameters
phone
stringrequired
The same number the code was sent to.
code
stringrequired
What your user typed in.
Shell
curl -X POST https://api.jkasms.com/api/v1/otp/verify \
-H "Authorization: Bearer $JKASMS_SECRET_KEY" \
-H "X-JKASMS-Org-Id: $JKASMS_ORG_ID" \
-H "Content-Type: application/json" \
-d '{ "phone": "0241234567", "code": "483921" }'Response200
JSON
{
"success": true,
"data": { "verified": true, "message": "Code verified." }
}A wrong code is not an error — it is an expected outcome you show to your user. It returns 200 with verified: false, so you branch on the field, not on the status code.
JSON
{
"success": true,
"data": {
"verified": false,
"reason": "invalid",
"message": "That code is incorrect."
}
}reason | Meaning |
|---|---|
invalid | Wrong code, or no code outstanding for that number |
expired | The code passed its expiry |
attempts_exhausted | Too many wrong guesses; the code has been burned |
Node.js
const res = await fetch("https://api.jkasms.com/api/v1/otp/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.JKASMS_SECRET_KEY}`,
"X-JKASMS-Org-Id": process.env.JKASMS_ORG_ID,
"Content-Type": "application/json",
},
body: JSON.stringify({ phone, code }),
});
const { data } = await res.json();
if (data.verified) {
await markPhoneVerified(userId, phone);
} else {
// data.message is already written for an end user.
showError(data.message);
}Built-in protections
- Single use. A code is burned on the first successful verification.
- One live code per number. Requesting a new code invalidates the previous one, so an attacker never has several valid guesses at once.
- Attempt cap. Exhausting attempts burns the code rather than letting a caller request a fresh one and keep guessing.
- Resend cooldown. One code per number per 60 seconds — this endpoint would otherwise be a free way to bombard any number.
- Rate limited to 30 requests a minute on a live key, so a stolen key cannot drain your balance at speed.
OTP messages ignore the opt-out list on purpose. Blocking a login code because someone once replied STOP to a marketing campaign would lock them out of their own account.