Skip to content

Send and verify an OTP

OTP is the shortest path through MauKirim: you ask for a code, the user receives it on WhatsApp, you verify what they typed. MauKirim generates the code, stores only a hash of it, and never returns it — not to you, not in a log.

Five things are true of every OTP challenge:

Property Value
Code length 6 digits
Lifetime 5 minutes
Attempts 5, then the challenge stops verifying
Rate limit 5 issues per hour per (account, phone number)
Delivery The code’s message is sent from a MauKirim-managed number
Terminal window
curl -X POST https://app.maukirim.com/api/v1/otp/send \
-H "Authorization: Bearer mk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup-8821" \
-d '{ "phone": "+6281234567890", "purpose": "signup" }'
{ "ok": true, "challengeId": "otp_…", "batchId": "gwb_…", "expiresAt": "2026-09-28T13:20:00.000Z" }
Field Required Notes
phone yes E.164. Separators (space . - ( )) are stripped before validation, so +62 812-3456-7890 is accepted
purpose yes login, signup, payment, passwordReset, phoneChange

purpose is stored with the challenge. The OTP message text is generated by MauKirim (code, expiry and do-not-share warning); it is not a customer notification template and carries no attachment. Immediately before dispatch, the managed number briefly shows a typing indicator in the recipient chat. Typing presence is best-effort and is not proof of acceptance or delivery; the OTP is verified only after the gateway accepts its message.

Keep challengeId next to your own session — it is the only handle for this code, and it belongs to the account that asked for it. A challenge id from another account answers 404 not_found.

Terminal window
curl -X POST https://app.maukirim.com/api/v1/otp/verify \
-H "Authorization: Bearer mk_live_…" \
-H "Content-Type: application/json" \
-d '{"challengeId": "otp_…", "code": "481902"}'
{ "ok": true, "verified": true }

verified: false is a normal answer, not an error. It is what you get for a wrong code, a code that was already used, a code past its five minutes, a challenge out of attempts, or a code whose WhatsApp message was never accepted by the gateway. The HTTP status is 200 in every one of those cases because the question “is this code right?” was answered: no.

Only these are errors:

Status Code When
400 invalid_input challengeId or code missing/blank, or a body over 8 KB
404 not_found No challenge with that id belongs to your account
429 rate_limited More than 5 issues to that number in an hour (on otp/send only)
402 insufficient_credits Balance below the cost of the OTP message
503 worker_offline Dispatch worker is not running — retry

A wrong guess consumes an attempt. After five, further verifies return verified: false even for the right code, so your UI must give the user a way to ask for a new code rather than letting them retry blindly.

Verifying is idempotent-ish, issuing is not

Section titled “Verifying is idempotent-ish, issuing is not”
  • POST /otp/verify needs no Idempotency-Key. Calling it twice with the right code returns verified: true the first time; after that the challenge is spent, so the second call is verified: false. Record your own “this user is verified” fact rather than re-verifying later.
  • POST /otp/send requires Idempotency-Key. Use an id derived from the signup/session it belongs to, so a double-submitted form does not send two messages — and remember the same key with a different phone number is a 409, not a second send.
1. User submits their phone number.
2. POST /otp/send with Idempotency-Key: "signup-<your session id>"
-> store challengeId on the session, start a 5-minute timer in your UI.
3. User types the six digits.
4. POST /otp/verify with { challengeId, code }.
- verified: true -> log them in, mark your own session verified, stop accepting codes for it.
- verified: false -> show "that code is not right", and after a few tries offer "send a new code".
5. If the user asks for a new code: POST /otp/send with a NEW Idempotency-Key
(for example "signup-<session>-2"), because the old key is reserved.

Two details that come up in every integration:

  • Do not resend on a timer. Five issues per hour per number is a real limit, and hitting 429 while a user waits is worse than a button they have to press. Let the user ask.
  • Do not treat the phone number as identity. A verified challenge means “whoever holds this phone number received a code”; binding it to the right account is your job (store the account id with the challenge id before you verify).