Send and verify OTP codes over WhatsApp. Use this page for quick integration and error handling.
/auth/start with the user phone number./auth/verify with the returned session_id and the OTP code entered by the user.curl -X POST https://api.loginwa.com/api/v1/auth/start \
-H "Authorization: Bearer <SECRET_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"phone":"6281234567890","meta":{"user_id":"123"}}'
curl -X POST https://api.loginwa.com/api/v1/auth/verify \
-H "Authorization: Bearer <SECRET_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"session_id":"<SESSION_ID>","otp_code":"123456"}'
Authorization: Bearer <SECRET_API_KEY> (or X-Api-Key).application/json.curl -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
https://api.loginwa.com/api/v1/auth/start
Use different keys per project to isolate traffic, usage, and logs.
Use the SDKs or plugin for the fastest integration path. For other stacks, call the REST endpoints below.
| Field | Type | Required | Example |
|---|---|---|---|
| phone | string | required | 6281234567890 |
| country_code | string | optional | 62 |
| otp_length | int | optional | 6 |
| message_template | string | optional | OTP code {code} |
| meta | object | optional | {"user_id":"123"} |
{
"session_id": "3bbaaf0b-3c11-44a2-8a7e-4edc426c5fcd",
"expires_in": 300,
"sent_via_engine": true
}
Errors: 401 unauthorized, 422 invalid_phone, 429 quota_exceeded.
| Field | Type | Required | Example |
|---|---|---|---|
| session_id | string | required | sess_xxx |
| otp_code | string | required | 123456 |
{
"status": "verified",
"phone": "6281234567890",
"verified_at": "2025-11-28T12:00:00Z"
}
Errors: 401 unauthorized, 422 invalid_code | expired | blocked.
quota_exceeded.Use dashboard logs to monitor success rate, failures, and per-app usage.
401 unauthorized — missing/invalid API key.422 invalid_phone — phone format cannot be parsed.422 invalid_code / expired / max_attempts — verification failed.429 quota_exceeded — monthly quota finished for this key.Handle 4xx gracefully and let users retry or request a new OTP after cooldown.
wa_message_logs (admin: /admin/wa-logs, filter by device/direction).401 unauthorized, 422 invalid_phone | invalid_code | expired | max_attempts, 429 quota_exceeded.| Code | HTTP | Meaning |
|---|---|---|
| invalid_phone | 422 | Phone format invalid |
| invalid_code | 422 | OTP incorrect |
| expired | 422 | OTP expired |
| max_attempts | 422 | Verification attempts exceeded |
| quota_exceeded | 429 | Plan quota exceeded |
| unauthorized | 401 | Invalid API key |
$client = new \GuzzleHttp\Client();
$res = $client->post('https://api.loginwa.com/api/v1/auth/start', [
'headers' => [ 'Authorization' => 'Bearer '.getenv('API_KEY') ],
'json' => [ 'phone' => '6281234567890' ]
]);
$body = json_decode((string) $res->getBody(), true);
const res = await fetch('https://api.loginwa.com/api/v1/auth/start', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ phone: '6281234567890' })
});
const data = await res.json();
curl -X POST https://api.loginwa.com/api/v1/auth/start \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"6281234567890"}'
Set header x-engine-hook-secret on the sender (engine). Verify it server-side:
// Laravel example
if (! hash_equals(config('wa_engine.secret'), $request->header('x-engine-hook-secret'))) {
abort(403, 'invalid signature');
}
Use different secrets per environment (staging/production) to avoid webhook mix-ups.
Need help with integration, higher limits, or production cutover?