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

FieldTypeDescription
torequiredstringGhana mobile number, local (024…) or international (+233…).
fromrequiredstringSender ID (up to 11 letters/numbers). Production needs an approved one; sandbox accepts any valid name.
bodyrequiredstringUp 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

JSON
{
  "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

CodeHTTPMeaning
VALIDATION_FAILED400Some fields are missing or invalid.
SMS_NOT_ENABLED409SMS is off for this project.
SENDER_NOT_APPROVED400Production messages need an approved sender ID.
RECIPIENT_SUPPRESSED400The number opted out of your messages.
INSUFFICIENT_FUNDS402Your wallet can't cover the price. Nothing was sent or charged.
PRICING_NOT_CONFIGURED503No price is set for this destination yet.
SMS_RATE_LIMITED429Project 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

FieldTypeDescription
fromrequiredstring
bodyrequiredstringMay use {{field}} placeholders filled from each recipient's fields; {{field|fallback}} sets a default.
namestringShown in the dashboard. Optional.
toarrayNumbers to send to (no personalization).
recipientsarrayNumbers with personalization fields. Use `to`, `recipients` or both.
scheduledAtstringSend 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

JSON
{
  "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

CodeHTTPMeaning
VALIDATION_FAILED400Some fields are missing or invalid.
SMS_NOT_ENABLED409SMS is off for this project.
SENDER_NOT_APPROVED400Production messages need an approved sender ID.
INSUFFICIENT_FUNDS402Your wallet can't cover the price. Nothing was sent or charged.
PRICING_NOT_CONFIGURED503No price is set for this destination yet.
TOO_MANY_RECIPIENTS400More 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

NameDescription
batchIdThe `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

JSON
{
  "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

CodeHTTPMeaning
BATCH_NOT_FOUND404No 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

NameDescription
messageIdThe `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

JSON
{
  "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

CodeHTTPMeaning
MESSAGE_NOT_FOUND404No 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

NameDescription
fromStart (ISO 8601). Default: 7 days ago.
toEnd (ISO 8601). Default: now.
statuspending, delivered, failed or cancelled.
typesingle, bulk or otp.
batchIdOnly messages from this bulk batch.
cursor`nextCursor` from the previous page.
limit1–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

JSON
{
  "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

CodeHTTPMeaning
VALIDATION_FAILED400Some 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

JSON
{
  "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

FieldTypeDescription
torequiredstring
purposestringYour label, e.g. login, signup, payment (shown in analytics).
referencestringYour own reference (e.g. user ID), returned with the verification and in webhooks.
lengthintegerDigits. Default from Verify settings (6).
expiresInintegerSeconds. Default from Verify settings (300).
senderstringSender 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

JSON
{
  "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

CodeHTTPMeaning
VALIDATION_FAILED400Some fields are missing or invalid.
SENDER_NOT_APPROVED400Production messages need an approved sender ID.
INSUFFICIENT_FUNDS402Your wallet can't cover the price. Nothing was sent or charged.
OTP_NOT_ENABLED409OTP is off for this project.
OTP_SENDER_NOT_CONFIGURED409No sender ID is set for production codes.
DESTINATION_NOT_ALLOWED422The number's network isn't allowed by your Verify settings.
OTP_RATE_LIMITED429Too many codes from this project this minute (or the sandbox daily limit).
OTP_PHONE_RATE_LIMITED429Too many codes sent to this number recently.
OTP_PHONE_BLOCKED429Too many wrong codes for this number; it's blocked for an hour.
SPEND_LIMIT_REACHED403The 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

FieldTypeDescription
requestIdrequiredstring
coderequiredstring

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

JSON
{
  "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

CodeHTTPMeaning
VALIDATION_FAILED400Some fields are missing or invalid.
OTP_PHONE_BLOCKED429Too many wrong codes for this number; it's blocked for an hour.
OTP_NOT_FOUND404No verification with this request ID for this project and environment.
OTP_INCORRECT422Wrong code. One attempt used.
OTP_EXPIRED410The code expired.
OTP_ALREADY_VERIFIED409This verification already succeeded; codes are single use.
OTP_MAX_ATTEMPTS429Too 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

FieldTypeDescription
requestIdrequiredstring

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

JSON
{
  "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

CodeHTTPMeaning
INSUFFICIENT_FUNDS402Your wallet can't cover the price. Nothing was sent or charged.
OTP_PHONE_RATE_LIMITED429Too many codes sent to this number recently.
OTP_NOT_FOUND404No verification with this request ID for this project and environment.
OTP_EXPIRED410The code expired.
OTP_ALREADY_VERIFIED409This verification already succeeded; codes are single use.
OTP_RESEND_COOLDOWN429A code was sent too recently.
OTP_RESEND_LIMIT429No 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

NameDescription
requestIdThe `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

JSON
{
  "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

CodeHTTPMeaning
OTP_NOT_FOUND404No verification with this request ID for this project and environment.