Getting Started
In this documentation, we will take you through a quick overview of how to use LightOTP. As you might expect, LightOTP is simple and developer-friendly, requiring minimal setup to get started.
In this guide, we will walk you through the essential steps, including how to purchase a plan and how to use API keys to start sending OTP messages.
The process is straightforward and designed to help you integrate OTP functionality into your application quickly and efficiently.
Purchase a Plan
Before sending any OTP messages, you need to subscribe to a plan.
Steps:
- Go to the "Subscriptions" section in your dashboard, and click "Purchase plan".
- Select a plan from the available, and click "Subscribe".
- Automated payment is currently unavailable. Click "Confirm Order", add optional notes if needed, and our team will contact you via WhatsApp or email to activate your plan.
Send OTP Messages
Steps:
- Go to the "API Keys" section in your dashboard, and add a new API key. Make sure the key is stored securely and not shared with anyone outside your team.
- Call the Send Message endpoint with the OTP code and recipient phone number.
- Persist the returned id so you can later look up delivery status via the Check Message Status endpoint.
Endpoint
API end point:
| Item | Value |
|---|---|
| Method | POST |
| Path | /SendMessage |
| Content-Type | application/json |
| Authentication | X-Api-Key HTTP header |
| Success status | 200 OK |
Authentication
Every request must include your API key in the X-Api-Key request header. API keys are issued per client from the LightOTP dashboard and may be assigned an optional expiration date. Requests with a missing, invalid, or expired key are rejected before any credits are consumed.
Treat the API key as a credential. Store it server-side, never expose it in mobile apps, browser code, or public repositories. If a key is compromised, revoke and rotate it from the dashboard immediately.
Headers
| Header | Required | Description |
|---|---|---|
X-Api-Key | Yes | Your client API key. |
Content-Type | Yes | Must be application/json. |
Accept-Language | No | ar or en. Controls the language of error and email notifications returned by the platform (does not select the WhatsApp message language — see languageCode). |
Body
Example body:
{
"otpCode": "123456",
"toPhoneE164": "+966551234567",
"languageCode": "en",
"idempotencyKey": "a3f1c2e4-9b27-4d6a-8e5f-1c2b3d4e5f60"
}| Field | Type | Required | Constraints |
|---|---|---|---|
otpCode | string | Yes | 1–8 characters. Letters (A–Z, a–z) and digits (0–9) only. No spaces or symbols. |
toPhoneE164 | string | Yes | Recipient phone number in E.164 format (e.g., +966551234567). |
languageCode | string | No | BCP-47 (e.g., en, ar, en_US, ar_SA). Selects the language of the WhatsApp message. If omitted or no matching language is available, the platform falls back to your account's default language. |
idempotencyKey | uuid | No | Optional client-supplied UUID for safe retries. See idempotencyKey below. |
otpCode — full validation rules
- Must be included in the request.
- Length must be ≤ 8 characters.
- Every character must be a letter or a digit. Hyphens, spaces, dots, and other punctuation are rejected.
toPhoneE164 — phone format
The number must be a valid number for its region; merely being well-formed (e.g., starting with + and the right digit count) is not enough.
Valid:
+966551234567+201001234567+14155552671
Invalid:
0551234567(no country code)+999999999999(not a valid region)
languageCode — language selection
If you provide a languageCode, the message is sent in that language. If no matching language is available, the platform falls back to your account's default language.
idempotencyKey — safe retries
When you supply an idempotencyKey, the platform stores it alongside the message it creates. If you later call this endpoint again with the same idempotencyKey under the same API key, the platform does not send a new message and does not charge your account again — it simply replays the original response, returning the same id and the same messageStatus you received the first time. This makes it safe to retry a request whose response you didn't receive (timeouts, dropped connections, transient network failures) without risking a duplicate send. If the replayed response shows messageStatus as Failed, the original message did not go through; reusing the same idempotencyKey will keep returning that failed result. To actually deliver the OTP in that case, generate a new idempotencyKey and submit the request again. Each idempotencyKey must be unique across all of your previous requests; once used, it is permanently bound to that one message and cannot be reused for a different one. Omit the field if you don't need this guarantee — the platform will accept the request without it.
Success Response — 200 OK
Example response:
{
"id": "f5b2d2e9-2c1f-4f1d-9c89-2c41f3d9a4e2",
"messageStatus": "Sent"
}| Field | Type | Description |
|---|---|---|
id | uuid | Unique LightOTP message identifier. Persist this value to look up delivery status later via CheckMessageStatus. |
messageStatus | string | Initial status returned by WhatsApp at the time of the call. One of: Pending, Sent, Delivered, Read, Failed, Deleted. Almost always Pending. |
messageStatus lifecycle
| Status | Meaning |
|---|---|
Pending | Created locally; not yet acknowledged by WhatsApp. |
Sent | Accepted by WhatsApp for delivery. |
Delivered | Delivered to the recipient device. |
Read | The recipient opened the chat containing the message. |
Failed | WhatsApp rejected the send or downstream delivery failed. |
Deleted | Message was deleted (administrative action). |
Use Check Message Status to read the latest state of a message.
Errors
All errors are returned as JSON in the form:
Example error:
{
"errorMessage": "InsufficientBalance"
}Error codes
| Code | HTTP | Meaning |
|---|---|---|
ApiKeyIsRequired | 400 | The X-Api-Key header was missing or empty. |
ApiKeyNotFound | 404 | No active API key matches the provided value. |
ApiKeyExpired | 400 | The API key has passed its expiration date. |
OTPCodeIsRequired | 400 | otpCode was missing. |
OTPCodeLengthMustBeLessThanOrEqualTo8 | 400 | otpCode exceeds 8 characters. |
OTPCodeMustContainOnlyLettersOrDigits | 400 | otpCode contains characters other than letters and digits. |
DestinationPhoneNumberIsRequired | 400 | toPhoneE164 was missing. |
InvalidphoneNumber | 400 | toPhoneE164 is not a valid E.164 phone number. |
PhoneNumberCanNotBeEmpty | 400 | toPhoneE164 was empty. |
InsufficientBalance | 400 | No active subscription has enough credits to cover the cost of this message for the destination country. |
SenderNumberNotFound | 404 | No sender number is assigned to your account. |
TemplateNotFound | 404 | No matching language could be found for this request. |
Duplicate-send cooldown message | 400 | The duplicate-send cooldown is active for this destination. See duplicate-send protection. |
WhatsApp service error | 4xx | WhatsApp service rejected the send. The errorMessage is the message returned by WhatsApp; the HTTP status mirrors WhatsApp's response status. |
InternalServerError | 500 | An unexpected error occurred. Please try again later, and if the issue persists, contact support. |
Billing & credits
Each successful send debits one or more credits from your most-recently created active subscription that has sufficient balance.
- Default cost: 1 credit per message.
- Credits are deducted based on the destination country according to our international pricing table. Countries not listed default to 1 credit per message.
- Failed messages are not charged from your balance, but are still recorded for tracking.
- When your subscription's credits are fully consumed, it is automatically marked as expired.
- We send you an email when your subscription's balance is running low.
Duplicate-send protection
To prevent repeatedly sending the same OTP to a recipient, the platform applies a wait time per recipient phone number, which grows the more you resend to the same number:
- Initial wait time: 30 seconds after a successful send.
- Each additional send within the rolling 6-hour window doubles the wait time: send #1 → 30s, send #2 → 60s, send #3 → 120s, send #4 → 240s, and so on.
- The wait time is capped at 6 hours.
- The wait time resets after 6 hours with no non-failed sends to that number.
- Failed messages do not count toward the wait time.
While the wait time is active, the API responds with 400 Bad Request and an errorMessage of the form: "You can't send another message to the same number yet. Please wait HH:mm:ss and try again."
Rate limiting
The platform applies per-IP request limits to prevent abuse. When the limit is exceeded, the API returns 429 Too Many Requests.
Example (cURL)
cURL:
curl -X POST "https://api.lightotp.com/SendMessage" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Accept-Language: en" \
-d '{
"otpCode": "482913",
"toPhoneE164": "+966551234567",
"languageCode": "en",
"idempotencyKey": "a3f1c2e4-9b27-4d6a-8e5f-1c2b3d4e5f60"
}'Check Message Status
After a successful send, use this endpoint to read the latest state of a message (including failure details if it failed).
API end point:
| Item | Value |
|---|---|
| Method | POST |
| Path | /CheckMessageStatus |
| Content-Type | application/json |
| Authentication | X-Api-Key HTTP header |
| Success status | 200 OK |
Request Body
Example body:
{
"id": "f5b2d2e9-2c1f-4f1d-9c89-2c41f3d9a4e2"
}Success Response — 200 OK
Example response:
{
"id": "f5b2d2e9-2c1f-4f1d-9c89-2c41f3d9a4e2",
"messageStatus": "Delivered",
"toPhoneE164": "+966551234567",
"createdAt": "2026-05-14T09:13:42.812Z",
"failureCode": null,
"failureTitle": null,
"failureReason": null,
"failedAt": null
}| Field | Type | Description |
|---|---|---|
id | uuid | The LightOTP message identifier. |
messageStatus | string | Current status: Pending, Sent, Delivered, Read, Failed, or Deleted. |
toPhoneE164 | string | The destination phone number the message was sent to. |
createdAt | datetime | UTC timestamp when the message was created. |
failureCode | integer | null | WhatsApp Graph API error code, if available. |
failureTitle | string | null | WhatsApp error type / title. |
failureReason | string | null | Human-readable failure reason. |
failedAt | datetime | null | UTC timestamp when the failure was recorded. |
Failure fields
When the message has failed, the failure fields are populated:
| Field | Type | Description |
|---|---|---|
failureCode | integer | null | WhatsApp Graph API error code, if available. |
failureTitle | string | null | WhatsApp error type / title. |
failureReason | string | null | Human-readable failure reason. |
failedAt | datetime | null | UTC timestamp when the failure was recorded. |
Errors
| Code | HTTP | Meaning |
|---|---|---|
ApiKeyIsRequired | 400 | The X-Api-Key header was missing. |
ApiKeyNotFound | 404 | No API key matches the provided value. |
ApiKeyExpired | 400 | The API key has expired. |
MessageNotFound | 404 | No message with that id exists for your client. |
Example (cURL)
cURL:
curl -X POST "https://api.lightotp.com/CheckMessageStatus" \
-H "Content-Type: application/json" \
-H "X-Api-Key: YOUR_API_KEY" \
-d '{ "id": "f5b2d2e9-2c1f-4f1d-9c89-2c41f3d9a4e2" }'Try it yourself
Head to your dashboard to generate an API key and send your first OTP, or contact us if you need help getting started.