API reference
Base URL https://api.nopexo.com. Generated from the same definition as the OpenAPI document.
Send an SMS
POST/v1/sms/send
Queues one message and returns immediately with status `queued`. `202 Accepted` is not delivery: follow the status with `GET /v1/sms/{messageId}`, a report, or the `message.*` webhooks. Production keys charge your wallet (the price is held when the message is queued and taken when the network accepts it; released if it isn't sent). Sandbox keys are free and never reach a phone.
Permission: sms:send · Supports Idempotency-Key
Body
| Field | Type | Description |
|---|---|---|
torequired | string | Ghana mobile number, local (024…) or international (+233…). |
fromrequired | string | Sender ID (up to 11 letters/numbers). Production needs an approved one; sandbox accepts any valid name. |
bodyrequired | string | Up to 6 SMS parts (918 GSM-7 or 402 Unicode characters). |
Example request
curl -X POST "https://api.nopexo.com/v1/sms/send" \
-H "Authorization: Bearer $NOPEXO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"to": "0200000009",
"from": "NOPEXO",
"body": "Your order #1042 is ready for pickup."
}'Response · 202
{
"id": "msg_7kd2qm4x9t3c8f1v",
"to": "+233200000009",
"network": "Telecel",
"from": "NOPEXO",
"body": "Your order #1042 is ready for pickup.",
"encoding": "GSM-7",
"segments": 1,
"status": "queued",
"final": false,
"type": "single",
"environment": "sandbox",
"simulated": true,
"charge": null,
"batchId": null,
"otpRequestId": null,
"error": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"submittedAt": null,
"completedAt": null
}Errors
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED | 400 | Some fields are missing or invalid. |
SMS_NOT_ENABLED | 409 | SMS is off for this project. |
SENDER_NOT_APPROVED | 400 | Production messages need an approved sender ID. |
RECIPIENT_SUPPRESSED | 400 | The number opted out of your messages. |
INSUFFICIENT_FUNDS | 402 | Your wallet can't cover the price. Nothing was sent or charged. |
PRICING_NOT_CONFIGURED | 503 | No price is set for this destination yet. |
SMS_RATE_LIMITED | 429 | Project or organization send limit reached. |
Send bulk SMS
POST/v1/sms/bulk
Sends one message to up to 10,000 numbers, now or at `scheduledAt` (up to 90 days ahead). Nopexo removes duplicates, skips invalid and opted-out numbers, personalizes `{{field}}` placeholders from each recipient's `fields`, and dispatches in the background on a rate-limited queue. The response is the batch with what was accepted and skipped; follow it with `GET /v1/sms/bulk/{batchId}` or the `bulk.completed` webhook.
Permission: sms:send · Supports Idempotency-Key
Body
| Field | Type | Description |
|---|---|---|
fromrequired | string | |
bodyrequired | string | May use {{field}} placeholders filled from each recipient's fields; {{field|fallback}} sets a default. |
name | string | Shown in the dashboard. Optional. |
to | array | Numbers to send to (no personalization). |
recipients | array | Numbers with personalization fields. Use `to`, `recipients` or both. |
scheduledAt | string | Send later (1 minute to 90 days ahead). Omit to send now. |
Example request
curl -X POST "https://api.nopexo.com/v1/sms/bulk" \
-H "Authorization: Bearer $NOPEXO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"from": "NOPEXO",
"body": "Hi {{first_name}}, your appointment is on {{date}}.",
"name": "November reminders",
"recipients": [
{
"to": "0200000009",
"fields": {
"first_name": "Ama",
"date": "12 Nov"
}
},
{
"to": "0241234567",
"fields": {
"first_name": "Kofi",
"date": "13 Nov"
}
}
]
}'Response · 202
{
"batchId": "cmp_3vb8n2k5q7w1x9z4",
"name": "November reminders",
"from": "NOPEXO",
"status": "queued",
"environment": "sandbox",
"scheduledAt": null,
"recipients": {
"total": 3,
"accepted": 2,
"skipped": 1
},
"skipped": [
{
"reason": "invalid",
"count": 1
}
],
"messages": {
"queued": 0,
"submitted": 0,
"delivered": 0,
"failed": 0,
"cancelled": 0
},
"estimatedSegments": 2,
"estimatedCost": {
"amount": "0.0000",
"currency": "GHS"
},
"failureReason": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"startedAt": null,
"completedAt": null
}Errors
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED | 400 | Some fields are missing or invalid. |
SMS_NOT_ENABLED | 409 | SMS is off for this project. |
SENDER_NOT_APPROVED | 400 | Production messages need an approved sender ID. |
INSUFFICIENT_FUNDS | 402 | Your wallet can't cover the price. Nothing was sent or charged. |
PRICING_NOT_CONFIGURED | 503 | No price is set for this destination yet. |
TOO_MANY_RECIPIENTS | 400 | More recipients than a batch allows. |
Get a bulk batch
GET/v1/sms/bulk/{batchId}
Progress of a bulk batch: recipients accepted and skipped, and message outcomes so far.
Permission: sms:read
Path parameters
| Name | Description |
|---|---|
batchId | The `batchId` returned by `POST /v1/sms/bulk`. |
Example request
curl -X GET "https://api.nopexo.com/v1/sms/bulk/cmp_3vb8n2k5q7w1x9z4" \
-H "Authorization: Bearer $NOPEXO_API_KEY"Response · 200
{
"batchId": "cmp_3vb8n2k5q7w1x9z4",
"name": "November reminders",
"from": "NOPEXO",
"status": "completed",
"environment": "sandbox",
"scheduledAt": null,
"recipients": {
"total": 3,
"accepted": 2,
"skipped": 1
},
"skipped": [
{
"reason": "invalid",
"count": 1
}
],
"messages": {
"queued": 0,
"submitted": 0,
"delivered": 2,
"failed": 0,
"cancelled": 0
},
"estimatedSegments": 2,
"estimatedCost": {
"amount": "0.0000",
"currency": "GHS"
},
"failureReason": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"startedAt": null,
"completedAt": null
}Errors
| Code | HTTP | Meaning |
|---|---|---|
BATCH_NOT_FOUND | 404 | No batch with this ID in this project and environment. |
Get a message
GET/v1/sms/{messageId}
The current status of one message. Statuses only move forward: `queued` → `sending` → `submitted` → `delivered` (or `undelivered`, `expired`, `rejected`, `not_sent`, `cancelled`).
Permission: sms:read
Path parameters
| Name | Description |
|---|---|
messageId | The `id` returned when the message was sent. |
Example request
curl -X GET "https://api.nopexo.com/v1/sms/msg_7kd2qm4x9t3c8f1v" \
-H "Authorization: Bearer $NOPEXO_API_KEY"Response · 200
{
"id": "msg_7kd2qm4x9t3c8f1v",
"to": "+233200000009",
"network": "Telecel",
"from": "NOPEXO",
"body": "Your order #1042 is ready for pickup.",
"encoding": "GSM-7",
"segments": 1,
"status": "delivered",
"final": true,
"type": "single",
"environment": "sandbox",
"simulated": true,
"charge": null,
"batchId": null,
"otpRequestId": null,
"error": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"submittedAt": "2026-11-08T10:15:01.000Z",
"completedAt": "2026-11-08T10:15:04.000Z"
}Errors
| Code | HTTP | Meaning |
|---|---|---|
MESSAGE_NOT_FOUND | 404 | No message with this ID in this project and environment. |
Delivery report
GET/v1/sms/reports
A summary (totals, delivery rate, parts and cost) for a period, plus the matching messages, newest first. Defaults to the last 7 days. Page with `cursor`.
Permission: sms:read
Query parameters
| Name | Description |
|---|---|
from | Start (ISO 8601). Default: 7 days ago. |
to | End (ISO 8601). Default: now. |
status | pending, delivered, failed or cancelled. |
type | single, bulk or otp. |
batchId | Only messages from this bulk batch. |
cursor | `nextCursor` from the previous page. |
limit | 1–100. Default 50. |
Example request
curl -X GET "https://api.nopexo.com/v1/sms/reports?status=delivered&limit=50" \
-H "Authorization: Bearer $NOPEXO_API_KEY"Response · 200
{
"period": {
"from": "2026-11-01T00:00:00.000Z",
"to": "2026-11-08T10:15:00.000Z"
},
"summary": {
"total": 120,
"delivered": 114,
"pending": 2,
"failed": 4,
"cancelled": 0,
"deliveryRate": 0.95,
"segments": 131,
"cost": {
"amount": "4.5850",
"currency": "GHS"
}
},
"items": [
{
"id": "msg_7kd2qm4x9t3c8f1v",
"to": "+233200000009",
"network": "Telecel",
"from": "NOPEXO",
"body": "Your order #1042 is ready for pickup.",
"encoding": "GSM-7",
"segments": 1,
"status": "delivered",
"final": true,
"type": "single",
"environment": "sandbox",
"simulated": true,
"charge": null,
"batchId": null,
"otpRequestId": null,
"error": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"submittedAt": null,
"completedAt": null
}
],
"nextCursor": "msg_2bn5c8x1z4v7m3k6"
}Errors
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED | 400 | Some fields are missing or invalid. |
Wallet balance
GET/v1/account/balance
Your organization's wallet. `available` can be spent; `reserved` is held for messages waiting to be sent. Sandbox keys can read it but never spend it.
Permission: account:read
Example request
curl -X GET "https://api.nopexo.com/v1/account/balance" \
-H "Authorization: Bearer $NOPEXO_API_KEY"Response · 200
{
"currency": "GHS",
"available": {
"amount": "48.2500",
"currency": "GHS"
},
"reserved": {
"amount": "0.0700",
"currency": "GHS"
},
"total": {
"amount": "48.3200",
"currency": "GHS"
},
"lowBalance": false,
"environment": "sandbox",
"sandbox": true,
"note": "Sandbox requests are free and never use this balance."
}Send a verification code
POST/v1/otp/send
Creates a verification and sends a one-time code by SMS. Keep the `requestId` and pass it to `/v1/otp/verify` with the code your user types. Nopexo never stores the code itself (only a keyed hash) and never returns it — except in sandbox, where `testCode` is included because sandbox never reaches a phone. Length, expiry, attempts and resends follow your Verify settings; `length` and `expiresIn` can be set per request.
Permission: otp:send · Supports Idempotency-Key
Body
| Field | Type | Description |
|---|---|---|
torequired | string | |
purpose | string | Your label, e.g. login, signup, payment (shown in analytics). |
reference | string | Your own reference (e.g. user ID), returned with the verification and in webhooks. |
length | integer | Digits. Default from Verify settings (6). |
expiresIn | integer | Seconds. Default from Verify settings (300). |
sender | string | Sender ID for this code. Default from Verify settings. |
Example request
curl -X POST "https://api.nopexo.com/v1/otp/send" \
-H "Authorization: Bearer $NOPEXO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"to": "0200000009",
"purpose": "login",
"reference": "user_42"
}'Response · 202
{
"requestId": "otp_5hj3k8m2n9p4q7r1",
"status": "pending",
"verified": false,
"to": "+233 20 ••• 0009",
"network": "Telecel",
"purpose": "login",
"reference": "user_42",
"environment": "sandbox",
"expiresAt": "2026-11-08T10:20:00.000Z",
"attemptsRemaining": 5,
"sendsRemaining": 2,
"resendAvailableAt": "2026-11-08T10:16:00.000Z",
"delivery": {
"status": "queued",
"errorCode": null
},
"charge": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"verifiedAt": null,
"testCode": "123456"
}Errors
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED | 400 | Some fields are missing or invalid. |
SENDER_NOT_APPROVED | 400 | Production messages need an approved sender ID. |
INSUFFICIENT_FUNDS | 402 | Your wallet can't cover the price. Nothing was sent or charged. |
OTP_NOT_ENABLED | 409 | OTP is off for this project. |
OTP_SENDER_NOT_CONFIGURED | 409 | No sender ID is set for production codes. |
DESTINATION_NOT_ALLOWED | 422 | The number's network isn't allowed by your Verify settings. |
OTP_RATE_LIMITED | 429 | Too many codes from this project this minute (or the sandbox daily limit). |
OTP_PHONE_RATE_LIMITED | 429 | Too many codes sent to this number recently. |
OTP_PHONE_BLOCKED | 429 | Too many wrong codes for this number; it's blocked for an hour. |
SPEND_LIMIT_REACHED | 403 | The project's daily OTP spending limit was reached. |
Check a code
POST/v1/otp/verify
Checks the code your user entered. On success the verification is closed: the same code can't be used again (`OTP_ALREADY_VERIFIED`). Each wrong code uses one attempt; after the last one the verification fails (`OTP_MAX_ATTEMPTS`). Expired codes are rejected (`OTP_EXPIRED`). Only trust `verified: true` from this endpoint.
Permission: otp:verify
Body
| Field | Type | Description |
|---|---|---|
requestIdrequired | string | |
coderequired | string |
Example request
curl -X POST "https://api.nopexo.com/v1/otp/verify" \
-H "Authorization: Bearer $NOPEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requestId": "otp_5hj3k8m2n9p4q7r1",
"code": "123456"
}'Response · 200
{
"requestId": "otp_5hj3k8m2n9p4q7r1",
"status": "verified",
"verified": true,
"to": "+233 20 ••• 0009",
"network": "Telecel",
"purpose": "login",
"reference": "user_42",
"environment": "sandbox",
"expiresAt": "2026-11-08T10:20:00.000Z",
"attemptsRemaining": 0,
"sendsRemaining": 0,
"resendAvailableAt": null,
"delivery": {
"status": "delivered",
"errorCode": null
},
"charge": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"verifiedAt": "2026-11-08T10:15:42.000Z"
}Errors
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED | 400 | Some fields are missing or invalid. |
OTP_PHONE_BLOCKED | 429 | Too many wrong codes for this number; it's blocked for an hour. |
OTP_NOT_FOUND | 404 | No verification with this request ID for this project and environment. |
OTP_INCORRECT | 422 | Wrong code. One attempt used. |
OTP_EXPIRED | 410 | The code expired. |
OTP_ALREADY_VERIFIED | 409 | This verification already succeeded; codes are single use. |
OTP_MAX_ATTEMPTS | 429 | Too many wrong codes for this verification. |
Resend a code
POST/v1/otp/resend
Sends a new code for the same verification. The previous code stops working. Allowed after the cooldown (`resendAvailableAt`) while sends remain (`sendsRemaining`). Each send is billed like the first.
Permission: otp:send
Body
| Field | Type | Description |
|---|---|---|
requestIdrequired | string |
Example request
curl -X POST "https://api.nopexo.com/v1/otp/resend" \
-H "Authorization: Bearer $NOPEXO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"requestId": "otp_5hj3k8m2n9p4q7r1"
}'Response · 202
{
"requestId": "otp_5hj3k8m2n9p4q7r1",
"status": "pending",
"verified": false,
"to": "+233 20 ••• 0009",
"network": "Telecel",
"purpose": "login",
"reference": "user_42",
"environment": "sandbox",
"expiresAt": "2026-11-08T10:20:00.000Z",
"attemptsRemaining": 5,
"sendsRemaining": 1,
"resendAvailableAt": "2026-11-08T10:16:00.000Z",
"delivery": {
"status": "queued",
"errorCode": null
},
"charge": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"verifiedAt": null,
"testCode": "123456"
}Errors
| Code | HTTP | Meaning |
|---|---|---|
INSUFFICIENT_FUNDS | 402 | Your wallet can't cover the price. Nothing was sent or charged. |
OTP_PHONE_RATE_LIMITED | 429 | Too many codes sent to this number recently. |
OTP_NOT_FOUND | 404 | No verification with this request ID for this project and environment. |
OTP_EXPIRED | 410 | The code expired. |
OTP_ALREADY_VERIFIED | 409 | This verification already succeeded; codes are single use. |
OTP_RESEND_COOLDOWN | 429 | A code was sent too recently. |
OTP_RESEND_LIMIT | 429 | No more resends for this verification. |
Get a verification
GET/v1/otp/{requestId}
Status of a verification (never the code): pending, verified, expired, failed or cancelled, with attempts and sends left and the SMS delivery status.
Permission: otp:verify
Path parameters
| Name | Description |
|---|---|
requestId | The `requestId` from `/v1/otp/send`. |
Example request
curl -X GET "https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1" \
-H "Authorization: Bearer $NOPEXO_API_KEY"Response · 200
{
"requestId": "otp_5hj3k8m2n9p4q7r1",
"status": "pending",
"verified": false,
"to": "+233 20 ••• 0009",
"network": "Telecel",
"purpose": "login",
"reference": "user_42",
"environment": "sandbox",
"expiresAt": "2026-11-08T10:20:00.000Z",
"attemptsRemaining": 5,
"sendsRemaining": 2,
"resendAvailableAt": "2026-11-08T10:16:00.000Z",
"delivery": {
"status": "delivered",
"errorCode": null
},
"charge": null,
"createdAt": "2026-11-08T10:15:00.000Z",
"verifiedAt": null
}Errors
| Code | HTTP | Meaning |
|---|---|---|
OTP_NOT_FOUND | 404 | No verification with this request ID for this project and environment. |