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 |
Issue a code
Section titled “Issue a code”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.
Verify what they typed
Section titled “Verify what they typed”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/verifyneeds noIdempotency-Key. Calling it twice with the right code returnsverified: truethe first time; after that the challenge is spent, so the second call isverified: false. Record your own “this user is verified” fact rather than re-verifying later.POST /otp/sendrequiresIdempotency-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 a409, not a second send.
A sequence that behaves
Section titled “A sequence that behaves”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
429while 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).