API v1 · REST · OTP over WhatsApp

API Documentation

Complete reference for integrating WhatsApp OTP authentication, messaging, devices, broadcast, and webhooks into your application.

Base URL: https://api.loginwa.com Auth: Bearer <API_KEY>

Pricing is in USD. Base URL is https://api.loginwa.com (also reachable at https://loginwa.com/api). Versioned endpoints live under /api/v1/…; the OTP product endpoints are /api/auth/start & /api/auth/verify.

Quickstart

  1. Log in → create an app → copy the Secret API Key.
  2. Make sure your subscription is active (Free plan works out of the box).
  3. Call POST /api/auth/start with the user's phone number.
  4. User receives the OTP via WhatsApp.
  5. Call POST /api/auth/verify with the session_id and OTP code.
  6. Handle the response (verified or error).
# Send OTP
curl -X POST https://api.loginwa.com/api/auth/start \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"6281234567890"}'

# Verify OTP
curl -X POST https://api.loginwa.com/api/auth/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"session_id":"SESSION_ID","otp_code":"123456"}'

Authentication

  • Use the Authorization: Bearer <API_KEY> header.
  • Alternative: X-Api-Key: <API_KEY> header.
  • Content type: application/json.
  • Rate limit: 120 requests/min per API key on /api/v1/*; 60/min on /api/auth/*. Per-key limits can be customized.
  • Every request requires a valid API key and an active subscription.
  • Never expose your API key in frontend/mobile apps.

For mobile/SPA apps, call LoginWA from your backend server to keep the API key secure.

// Example header
Authorization: Bearer sk_live_abc123xyz789
// or
X-Api-Key: sk_live_abc123xyz789

OTP Endpoints

The WhatsApp OTP product. These endpoints now require a valid API key and an active subscription (rate-limited to 60 requests/min). Note the base path is /api/auth/… (not /api/v1/auth/…).

POST /api/auth/start

Send an OTP to a phone number via WhatsApp.

ParameterTypeRequiredDescription
phonestringYesPhone number with country code (e.g., 6281234567890)
country_codestringNoDefault country code if not in phone
otp_lengthintNoOTP length, 4–8 (default: 6)
message_templatestringNoCustom message with {code} placeholder
metaobjectNoCustom metadata to track

Response 200

{
  "session_id": "3bbaaf0b-3c11-44a2-8a7e-4edc426c5fcd",
  "expires_in": 300,
  "debug_code": null
}

debug_code is always null in production; the real code is only delivered over WhatsApp.

POST /api/auth/verify

Verify the OTP code entered by the user.

ParameterTypeRequiredDescription
session_idstringYesSession ID from /api/auth/start
otp_codestringYesOTP code entered by user

Response 200

{
  "status": "verified",
  "phone_number": "6281234567890",
  "verified_at": "2026-06-01T12:00:00Z"
}

A failed check returns 422 with status invalid_code, expired, or max_attempts.

Messages API

Send custom WhatsApp messages beyond OTP.

POST /api/v1/messages/send

Send a WhatsApp text message to a phone number. Each successful send deducts one message from your quota.

ParameterTypeRequiredDescription
phonestringYesRecipient phone with country code (10–20 chars)
messagestringYesMessage content, 1–4096 chars
device_idstringNoSpecific device to send from (auto-selected if omitted)
metaobjectNoCustom metadata attached to the message log

Request

curl -X POST https://api.loginwa.com/api/v1/messages/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"6281234567890","message":"Hello from LoginWA!"}'

Response 200

{
  "success": true,
  "message_id": "msg_abc123",
  "trace_id": "9f1c2e7a-1b3d-4f5a-9c8e-2a1b3c4d5e6f",
  "device_id": "dev_abc123",
  "phone": "6281234567890",
  "quota_remaining": 4987
}

Errors: 422 validation_failed, 429 quota_exceeded, 502 send_failed, 503 no_device_connected / no_device_available. See Error Codes.

Code Samples — Send Message

Calling POST /api/v1/messages/send in four languages. Keep your API key on the server side.

cURL

curl -X POST https://api.loginwa.com/api/v1/messages/send \
  -H "Authorization: Bearer $LOGINWA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "6281234567890",
    "message": "Hello from LoginWA!"
  }'

PHP (Guzzle)

$client = new \GuzzleHttp\Client();

$res = $client->post('https://api.loginwa.com/api/v1/messages/send', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('LOGINWA_API_KEY'),
        'Content-Type'  => 'application/json',
    ],
    'json' => [
        'phone'   => '6281234567890',
        'message' => 'Hello from LoginWA!',
    ],
]);

$data = json_decode((string) $res->getBody(), true);
echo $data['message_id'];

Node.js (fetch)

const res = await fetch(
  'https://api.loginwa.com/api/v1/messages/send',
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.LOGINWA_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      phone: '6281234567890',
      message: 'Hello from LoginWA!',
    }),
  }
);

if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data.message_id);

Python (requests)

import os, requests

res = requests.post(
    'https://api.loginwa.com/api/v1/messages/send',
    headers={
        'Authorization': f"Bearer {os.environ['LOGINWA_API_KEY']}",
        'Content-Type': 'application/json',
    },
    json={
        'phone': '6281234567890',
        'message': 'Hello from LoginWA!',
    },
)
res.raise_for_status()
print(res.json()['message_id'])

Devices API

Manage WhatsApp devices connected to your app. Device IDs use the engine_device_id (e.g. dev_abc123). Pairing is asynchronous: creating or refreshing a device returns 202 and you poll GET /api/v1/devices/{id} for the QR token.

GET /api/v1/devices

List all devices for your app.

// Response 200
{
  "devices": [
    {
      "id": "dev_abc123",
      "label": "My WhatsApp",
      "status": "online",
      "phone_number": "6281234567890",
      "is_online": true,
      "last_seen_at": "2026-06-01T10:00:00+00:00",
      "created_at": "2026-01-15T10:00:00+00:00"
    }
  ],
  "count": 1
}
POST /api/v1/devices

Register a new device and start QR pairing.

ParameterTypeRequired
labelstringNo
// Response 202
{
  "device_id": "dev_abc123",
  "engine_device_id": "dev_abc123",
  "label": "My WhatsApp",
  "status": "qr_waiting",
  "qr_code": "2@A1b2C3...",
  "job_id": "job_xyz",
  "qr_status_url": "https://api.loginwa.com/api/v1/devices/dev_abc123",
  "message": "Scan QR code with WhatsApp to connect device"
}
GET /api/v1/devices/{id}

Get device status, including the QR token while status is qr_waiting.

POST /api/v1/devices/{id}/refresh-qr

Request a fresh QR code (only when qr_waiting / offline / registering). Returns 202.

DELETE /api/v1/devices/{id}

Disconnect and remove a device.

Broadcast API

Send bulk WhatsApp messages to multiple recipients with smart delay, scheduling, and progress tracking. All responses are wrapped in { "success": true, "data": … }.

Key Features

  • Send to up to 10,000 contacts per campaign
  • Variable substitution: Hello {name}!
  • Media support (image, video, document, audio)
  • Smart delay between messages (anti-ban)
  • Schedule campaigns for later
  • Pause/Resume capability
  • Real-time progress tracking

Campaign Status

StatusDescription
draftCampaign created, not started
queuedWaiting to be processed
sendingCurrently sending messages
pausedTemporarily stopped
completedAll messages sent
failedCampaign failed

Broadcast Endpoints

GET /api/v1/broadcast/campaigns

List all your broadcast campaigns (paginated, 20/page).

POST /api/v1/broadcast/campaigns

Create a new broadcast campaign. Returns 201; returns 402 if contacts exceed remaining quota.

ParameterTypeRequired
namestringYes
messagestringYes
contactsarray (1–10000)Yes
contacts.*.phonestringYes
contacts.*.namestringNo
contacts.*.variablesobjectNo
media_urlstringNo
media_typestringNo (image/video/document/audio)
delay_secondsintegerNo (1–60, default: 5)
schedule_atdatetimeNo (must be in the future)
GET /api/v1/broadcast/campaigns/{id}

Get campaign details and progress.

POST /api/v1/broadcast/campaigns/{id}/send

Start sending the campaign (requires an online device).

POST /api/v1/broadcast/campaigns/{id}/pause

Pause a campaign that is currently sending.

POST /api/v1/broadcast/campaigns/{id}/resume

Resume a paused campaign.

GET /api/v1/broadcast/campaigns/{id}/contacts

List contacts with delivery status. Filter with ?status=sent|failed|pending

DELETE /api/v1/broadcast/campaigns/{id}

Delete a campaign (cannot delete while sending — pause first).

Example: Create & Send Campaign

// 1. Create campaign
POST /api/v1/broadcast/campaigns
{
  "name": "Summer Sale 2026",
  "message": "Hello {name}!\n\nSpecial offer for you:\n- 50% discount\n- Free shipping\n\nClick: https://shop.com/promo",
  "contacts": [
    {"phone": "081234567890", "name": "Budi"},
    {"phone": "081234567891", "name": "Ani", "variables": {"city": "Jakarta"}},
    {"phone": "081234567892", "name": "Citra"}
  ],
  "delay_seconds": 5,
  "media_url": "https://example.com/promo.jpg",
  "media_type": "image"
}

// Response
{
  "success": true,
  "data": {
    "id": "camp_abc123xyz",
    "name": "Summer Sale 2026",
    "status": "draft",
    "total_contacts": 3,
    "estimated_duration": "15 seconds"
  }
}

// 2. Start sending
POST /api/v1/broadcast/campaigns/camp_abc123xyz/send

// 3. Check progress
GET /api/v1/broadcast/campaigns/camp_abc123xyz
{
  "success": true,
  "data": {
    "id": "camp_abc123xyz",
    "status": "sending",
    "progress": {
      "total": 3,
      "sent": 2,
      "delivered": 1,
      "failed": 0,
      "pending": 1,
      "percentage": 66.7
    }
  }
}

Webhooks API

Configure webhooks to receive real-time events (incoming messages, delivery status, device and OTP events). Each app may register up to 5 webhooks. The signing secret is shown only once at creation and via regenerate-secret.

Webhook Events

EventDescription
message.incomingNew incoming WhatsApp message
message.sentMessage successfully sent
message.deliveredMessage delivered to recipient
message.readMessage read by recipient
message.failedMessage delivery failed
device.connectedDevice paired successfully
device.disconnectedDevice disconnected
device.qr_readyA QR code is ready to scan
otp.verifiedOTP verification successful
otp.expiredOTP session expired

Omit events (or send an empty array) to subscribe to all events.

Inbound Delivery & Payload

LoginWA POSTs JSON to your URL with headers X-LoginWA-Event and X-LoginWA-Signature (HMAC-SHA256 of the raw body using your secret).

{
  "event": "message.incoming",
  "timestamp": "2026-06-01T12:00:00+00:00",
  "data": {
    "message_id": "msg_abc123",
    "from": "6281234567890",
    "body": "Hello!",
    "device_id": "dev_xyz"
  }
}

Webhook Endpoints

GET /api/v1/webhooks

List all webhooks (also returns available_events).

POST /api/v1/webhooks

Create a new webhook. Returns 201 with the secret (shown once).

ParameterTypeRequired
urlstring (public https)Yes
eventsarrayNo
secretstringNo (auto-generated)
retry_countintegerNo (0–10, default: 3)
GET /api/v1/webhooks/{id}

Get webhook details and delivery health.

PUT /api/v1/webhooks/{id}

Update url, events, active, secret, or retry_count.

DELETE /api/v1/webhooks/{id}

Delete a webhook.

POST /api/v1/webhooks/{id}/test

Send a signed webhook.test event to the URL.

POST /api/v1/webhooks/{id}/regenerate-secret

Generate a new signing secret.

Verifying Webhook Signatures

Each delivery includes a signature in the X-LoginWA-Signature header (HMAC-SHA256 of the raw request body). Verify it to ensure authenticity:

// PHP/Laravel example
$signature = $request->header('X-LoginWA-Signature');
$payload   = $request->getContent();
$secret    = 'your_webhook_secret';

$expected = hash_hmac('sha256', $payload, $secret);

if (! hash_equals($expected, (string) $signature)) {
    abort(403, 'Invalid signature');
}

IP Whitelist API

Restrict an API key to specific source IP addresses. When restriction is enabled, requests from non-whitelisted IPs are rejected with 403 ip_not_allowed. These endpoints are exempt from the IP check so you can always manage your list.

GET /api/v1/ip-whitelist

List whitelisted IPs and whether restriction is enabled.

POST /api/v1/ip-whitelist

Add an IP. Body: ip_address (required), label (optional). Returns 409 if it already exists.

DELETE /api/v1/ip-whitelist/{id}

Remove an IP from the whitelist.

POST /api/v1/ip-whitelist/toggle

Enable/disable restriction. Body: enabled (boolean). Returns 422 if enabling with no IPs.

Auto-Reply & Chatbot

Automatic replies and chatbot flows are configured per app in the Dashboard — there is no separate API to manage them. When an inbound message arrives, LoginWA evaluates your rules and may reply automatically, then still delivers the message.incoming event to your webhooks so your own logic can run too.

Manage auto-reply in the Dashboard →

Billing & QRIS

Pricing is in USD. Plans and quotas are managed in the Dashboard, not via the API. Payments — including QRIS for Indonesian customers and card/PayPal globally — are handled entirely in the billing UI; there is no public billing API. An inactive, suspended, or past-due subscription causes API requests to return 402 subscription_suspended.

Manage Billing →

SDKs & Downloads

GitHub Repository

Official SDKs, examples, and Postman collection.

View on GitHub →

SDK Bundle ZIP

Download all SDKs in one package.

Download SDK.zip →

WordPress Plugin

Add WhatsApp OTP to WP login/register.

Download Plugin.zip →

Android SDK

Kotlin SDK with sample app.

View Documentation →

OTP Code Examples

Sending an OTP via POST /api/auth/start. Verify with POST /api/auth/verify using the returned session_id.

PHP (Guzzle)

$client = new \GuzzleHttp\Client();

// Send OTP
$res = $client->post('https://api.loginwa.com/api/auth/start', [
    'headers' => [
        'Authorization' => 'Bearer ' . getenv('LOGINWA_API_KEY'),
        'Content-Type' => 'application/json',
    ],
    'json' => [
        'phone' => '6281234567890',
        'otp_length' => 6,
    ],
]);

$data = json_decode((string) $res->getBody(), true);
$sessionId = $data['session_id'];

Node.js (fetch)

const BASE_URL = 'https://api.loginwa.com/api';
const API_KEY = process.env.LOGINWA_API_KEY;

async function sendOtp(phone) {
    const res = await fetch(`${BASE_URL}/auth/start`, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${API_KEY}`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ phone }),
    });

    if (!res.ok) throw new Error('Failed to send OTP');
    return res.json();
}

Python (requests)

import requests
import os

BASE_URL = 'https://api.loginwa.com/api'
API_KEY = os.environ.get('LOGINWA_API_KEY')

def send_otp(phone):
    res = requests.post(
        f'{BASE_URL}/auth/start',
        headers={
            'Authorization': f'Bearer {API_KEY}',
            'Content-Type': 'application/json',
        },
        json={'phone': phone}
    )
    res.raise_for_status()
    return res.json()

cURL

# Send OTP
curl -X POST https://api.loginwa.com/api/auth/start \
  -H "Authorization: Bearer $LOGINWA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"6281234567890"}'

# Verify OTP
curl -X POST https://api.loginwa.com/api/auth/verify \
  -H "Authorization: Bearer $LOGINWA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"session_id":"xxx","otp_code":"123456"}'

Pricing Plans

Start free, then scale as you grow. Prices are in USD. All plans include full API access, Widget, SDK, Dashboard, Webhooks, and unlimited projects.

Free
$0/mo
100 messages
Popular
Starter
$19/mo
5,000 messages
Business
$49/mo
15,000 messages
Enterprise
$149/mo
50,000 messages
Start Free → Manage Billing

Rate Limits & Quotas

Limit TypeValue
Rate limit — /api/v1/*120 requests/minute per API key
Rate limit — /api/auth/*60 requests/minute per API key
OTP Length6 digits (4–8 configurable)
OTP TTL5 minutes (300 seconds)
Max Verify Attempts5 attempts
Monthly QuotaBased on your plan (see pricing)

Per-key limits can be customized. Exceeding the rate limit returns 429 rate_limited with a Retry-After header. Exceeding quota returns 429 quota_exceeded.

Error Codes

CodeHTTPDescription
unauthorized401Invalid or missing API key
subscription_suspended402Subscription inactive, suspended, or past due
ip_not_allowed403Source IP not in the API key whitelist
validation_failed422Request body failed validation (see errors)
invalid_code422OTP code is incorrect
expired422OTP session has expired
max_attempts422Too many verification attempts
quota_exceeded429Monthly message quota exceeded
rate_limited429Too many requests per minute
send_failed502WhatsApp engine failed to send the message
no_device_available503No connected WhatsApp device available

Error Shape

// 422 validation_failed
{
  "error": "validation_failed",
  "message": "Invalid request parameters",
  "errors": { "phone": ["The phone field is required."] }
}

// 429 quota_exceeded
{ "error": "quota_exceeded", "message": "Monthly message quota exceeded", "quota_remaining": 0 }

// 403 ip_not_allowed
{ "error": "ip_not_allowed", "message": "Your IP address is not whitelisted for this API key.", "ip": "203.0.113.5" }

Need Help?

Having trouble with integration? We're here to help.

Email: [email protected] Dashboard & Logs View Pricing API Status