{"openapi":"3.1.0","info":{"title":"Nopexo API","version":"1.0.0","description":"SMS, bulk SMS and OTP verification for Ghana. Authenticate with a project API key: `Authorization: Bearer nxk_test_…` (sandbox) or `nxk_live_…` (production).","contact":{"name":"Nopexo support","url":"https://nopexo.com"}},"servers":[{"url":"https://api.nopexo.com"}],"security":[{"apiKey":[]}],"tags":[{"name":"SMS","description":"Send messages and follow their delivery."},{"name":"OTP","description":"Send and check one-time verification codes."},{"name":"Account","description":"Wallet balance."}],"paths":{"/v1/sms/send":{"post":{"operationId":"sendSms","tags":["SMS"],"summary":"Send an SMS","description":"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.\n\nRequired permission: `sms:send`.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Repeat a request safely: the same key returns the original result (200) instead of sending again.","schema":{"type":"string","minLength":8,"maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendSmsRequest"},"example":{"to":"0200000009","from":"NOPEXO","body":"Your order #1042 is ready for pickup."}}}},"responses":{"202":{"description":"Accepted (queued)","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"},"example":{"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}}}},"400":{"description":"VALIDATION_FAILED, SENDER_NOT_APPROVED, RECIPIENT_SUPPRESSED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"INSUFFICIENT_FUNDS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"SMS_NOT_ENABLED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED, SMS_RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"PRICING_NOT_CONFIGURED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X POST \"https://api.nopexo.com/v1/sms/send\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"to\": \"0200000009\",\n    \"from\": \"NOPEXO\",\n    \"body\": \"Your order #1042 is ready for pickup.\"\n  }'"},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/sms/send\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\": crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n    \"to\": \"0200000009\",\n    \"from\": \"NOPEXO\",\n    \"body\": \"Your order #1042 is ready for pickup.\"\n  }),\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/sms/send');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n        'Content-Type: application/json',\n        'Idempotency-Key: ' . bin2hex(random_bytes(16)),\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'to' => '0200000009',\n        'from' => 'NOPEXO',\n        'body' => 'Your order #1042 is ready for pickup.',\n    ]),\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])\n    ->acceptJson()\n    ->post('https://api.nopexo.com/v1/sms/send', [\n        'to' => '0200000009',\n        'from' => 'NOPEXO',\n        'body' => 'Your order #1042 is ready for pickup.',\n    ]);\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os, uuid\nimport requests\n\nres = requests.post(\n    \"https://api.nopexo.com/v1/sms/send\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n        \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\n        \"to\": \"0200000009\",\n        \"from\": \"NOPEXO\",\n        \"body\": \"Your order #1042 is ready for pickup.\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/sms/bulk":{"post":{"operationId":"sendBulkSms","tags":["SMS"],"summary":"Send bulk SMS","description":"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.\n\nRequired permission: `sms:send`.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Repeat a request safely: the same key returns the original result (200) instead of sending again.","schema":{"type":"string","minLength":8,"maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkSmsRequest"},"example":{"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"}}]}}}},"responses":{"202":{"description":"Accepted (queued)","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Batch"},"example":{"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}}}},"400":{"description":"VALIDATION_FAILED, SENDER_NOT_APPROVED, TOO_MANY_RECIPIENTS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"INSUFFICIENT_FUNDS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"SMS_NOT_ENABLED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"PRICING_NOT_CONFIGURED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X POST \"https://api.nopexo.com/v1/sms/bulk\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"from\": \"NOPEXO\",\n    \"body\": \"Hi {{first_name}}, your appointment is on {{date}}.\",\n    \"name\": \"November reminders\",\n    \"recipients\": [\n      {\n        \"to\": \"0200000009\",\n        \"fields\": {\n          \"first_name\": \"Ama\",\n          \"date\": \"12 Nov\"\n        }\n      },\n      {\n        \"to\": \"0241234567\",\n        \"fields\": {\n          \"first_name\": \"Kofi\",\n          \"date\": \"13 Nov\"\n        }\n      }\n    ]\n  }'"},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/sms/bulk\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\": crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n    \"from\": \"NOPEXO\",\n    \"body\": \"Hi {{first_name}}, your appointment is on {{date}}.\",\n    \"name\": \"November reminders\",\n    \"recipients\": [\n      {\n        \"to\": \"0200000009\",\n        \"fields\": {\n          \"first_name\": \"Ama\",\n          \"date\": \"12 Nov\"\n        }\n      },\n      {\n        \"to\": \"0241234567\",\n        \"fields\": {\n          \"first_name\": \"Kofi\",\n          \"date\": \"13 Nov\"\n        }\n      }\n    ]\n  }),\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/sms/bulk');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n        'Content-Type: application/json',\n        'Idempotency-Key: ' . bin2hex(random_bytes(16)),\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'from' => 'NOPEXO',\n        'body' => 'Hi {{first_name}}, your appointment is on {{date}}.',\n        'name' => 'November reminders',\n        'recipients' => [\n            [\n                'to' => '0200000009',\n                'fields' => [\n                    'first_name' => 'Ama',\n                    'date' => '12 Nov',\n                ],\n            ],\n            [\n                'to' => '0241234567',\n                'fields' => [\n                    'first_name' => 'Kofi',\n                    'date' => '13 Nov',\n                ],\n            ],\n        ],\n    ]),\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])\n    ->acceptJson()\n    ->post('https://api.nopexo.com/v1/sms/bulk', [\n        'from' => 'NOPEXO',\n        'body' => 'Hi {{first_name}}, your appointment is on {{date}}.',\n        'name' => 'November reminders',\n        'recipients' => [\n            [\n                'to' => '0200000009',\n                'fields' => [\n                    'first_name' => 'Ama',\n                    'date' => '12 Nov',\n                ],\n            ],\n            [\n                'to' => '0241234567',\n                'fields' => [\n                    'first_name' => 'Kofi',\n                    'date' => '13 Nov',\n                ],\n            ],\n        ],\n    ]);\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os, uuid\nimport requests\n\nres = requests.post(\n    \"https://api.nopexo.com/v1/sms/bulk\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n        \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\n        \"from\": \"NOPEXO\",\n        \"body\": \"Hi {{first_name}}, your appointment is on {{date}}.\",\n        \"name\": \"November reminders\",\n        \"recipients\": [\n            {\n                \"to\": \"0200000009\",\n                \"fields\": {\n                    \"first_name\": \"Ama\",\n                    \"date\": \"12 Nov\",\n                },\n            },\n            {\n                \"to\": \"0241234567\",\n                \"fields\": {\n                    \"first_name\": \"Kofi\",\n                    \"date\": \"13 Nov\",\n                },\n            },\n        ],\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/sms/bulk/{batchId}":{"get":{"operationId":"getBatch","tags":["SMS"],"summary":"Get a bulk batch","description":"Progress of a bulk batch: recipients accepted and skipped, and message outcomes so far.\n\nRequired permission: `sms:read`.","parameters":[{"name":"batchId","in":"path","required":true,"description":"The `batchId` returned by `POST /v1/sms/bulk`.","schema":{"type":"string"},"example":"cmp_3vb8n2k5q7w1x9z4"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Batch"},"example":{"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}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"BATCH_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X GET \"https://api.nopexo.com/v1/sms/bulk/cmp_3vb8n2k5q7w1x9z4\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\""},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/sms/bulk/cmp_3vb8n2k5q7w1x9z4\", {\n  method: \"GET\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/sms/bulk/cmp_3vb8n2k5q7w1x9z4');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'GET',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n    ],\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->get('https://api.nopexo.com/v1/sms/bulk/cmp_3vb8n2k5q7w1x9z4');\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://api.nopexo.com/v1/sms/bulk/cmp_3vb8n2k5q7w1x9z4\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/sms/{messageId}":{"get":{"operationId":"getSms","tags":["SMS"],"summary":"Get a message","description":"The current status of one message. Statuses only move forward: `queued` → `sending` → `submitted` → `delivered` (or `undelivered`, `expired`, `rejected`, `not_sent`, `cancelled`).\n\nRequired permission: `sms:read`.","parameters":[{"name":"messageId","in":"path","required":true,"description":"The `id` returned when the message was sent.","schema":{"type":"string"},"example":"msg_7kd2qm4x9t3c8f1v"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"},"example":{"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"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"MESSAGE_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X GET \"https://api.nopexo.com/v1/sms/msg_7kd2qm4x9t3c8f1v\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\""},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/sms/msg_7kd2qm4x9t3c8f1v\", {\n  method: \"GET\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/sms/msg_7kd2qm4x9t3c8f1v');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'GET',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n    ],\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->get('https://api.nopexo.com/v1/sms/msg_7kd2qm4x9t3c8f1v');\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://api.nopexo.com/v1/sms/msg_7kd2qm4x9t3c8f1v\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/sms/reports":{"get":{"operationId":"smsReports","tags":["SMS"],"summary":"Delivery report","description":"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`.\n\nRequired permission: `sms:read`.","parameters":[{"name":"from","in":"query","required":false,"description":"Start (ISO 8601). Default: 7 days ago.","schema":{"type":"string","format":"date-time"},"example":"2026-11-01T00:00:00Z"},{"name":"to","in":"query","required":false,"description":"End (ISO 8601). Default: now.","schema":{"type":"string","format":"date-time"}},{"name":"status","in":"query","required":false,"description":"pending, delivered, failed or cancelled.","schema":{"type":"string","enum":["pending","delivered","failed","cancelled"]}},{"name":"type","in":"query","required":false,"description":"single, bulk or otp.","schema":{"type":"string","enum":["single","bulk","otp"]}},{"name":"batchId","in":"query","required":false,"description":"Only messages from this bulk batch.","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"`nextCursor` from the previous page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"1–100. Default 50.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Report"},"example":{"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"}}}},"400":{"description":"VALIDATION_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X GET \"https://api.nopexo.com/v1/sms/reports?status=delivered&limit=50\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\""},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/sms/reports?status=delivered&limit=50\", {\n  method: \"GET\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/sms/reports?status=delivered&limit=50');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'GET',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n    ],\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->get('https://api.nopexo.com/v1/sms/reports?status=delivered&limit=50');\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://api.nopexo.com/v1/sms/reports?status=delivered&limit=50\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/account/balance":{"get":{"operationId":"accountBalance","tags":["Account"],"summary":"Wallet balance","description":"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.\n\nRequired permission: `account:read`.","parameters":[],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"},"example":{"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."}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X GET \"https://api.nopexo.com/v1/account/balance\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\""},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/account/balance\", {\n  method: \"GET\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/account/balance');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'GET',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n    ],\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->get('https://api.nopexo.com/v1/account/balance');\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://api.nopexo.com/v1/account/balance\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/otp/send":{"post":{"operationId":"sendOtp","tags":["OTP"],"summary":"Send a verification code","description":"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.\n\nRequired permission: `otp:send`.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Repeat a request safely: the same key returns the original result (200) instead of sending again.","schema":{"type":"string","minLength":8,"maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendOtpRequest"},"example":{"to":"0200000009","purpose":"login","reference":"user_42"}}}},"responses":{"202":{"description":"Accepted (queued)","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpRequest"},"example":{"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"}}}},"400":{"description":"VALIDATION_FAILED, SENDER_NOT_APPROVED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"INSUFFICIENT_FUNDS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE, SPEND_LIMIT_REACHED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"OTP_NOT_ENABLED, OTP_SENDER_NOT_CONFIGURED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"DESTINATION_NOT_ALLOWED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED, OTP_RATE_LIMITED, OTP_PHONE_RATE_LIMITED, OTP_PHONE_BLOCKED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X POST \"https://api.nopexo.com/v1/otp/send\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"to\": \"0200000009\",\n    \"purpose\": \"login\",\n    \"reference\": \"user_42\"\n  }'"},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/otp/send\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\": crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n    \"to\": \"0200000009\",\n    \"purpose\": \"login\",\n    \"reference\": \"user_42\"\n  }),\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/otp/send');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n        'Content-Type: application/json',\n        'Idempotency-Key: ' . bin2hex(random_bytes(16)),\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'to' => '0200000009',\n        'purpose' => 'login',\n        'reference' => 'user_42',\n    ]),\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->withHeaders(['Idempotency-Key' => (string) Str::uuid()])\n    ->acceptJson()\n    ->post('https://api.nopexo.com/v1/otp/send', [\n        'to' => '0200000009',\n        'purpose' => 'login',\n        'reference' => 'user_42',\n    ]);\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os, uuid\nimport requests\n\nres = requests.post(\n    \"https://api.nopexo.com/v1/otp/send\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n        \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\n        \"to\": \"0200000009\",\n        \"purpose\": \"login\",\n        \"reference\": \"user_42\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/otp/verify":{"post":{"operationId":"verifyOtp","tags":["OTP"],"summary":"Check a code","description":"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.\n\nRequired permission: `otp:verify`.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyOtpRequest"},"example":{"requestId":"otp_5hj3k8m2n9p4q7r1","code":"123456"}}}},"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpRequest"},"example":{"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"}}}},"400":{"description":"VALIDATION_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"OTP_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"OTP_ALREADY_VERIFIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"OTP_EXPIRED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"OTP_INCORRECT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED, OTP_MAX_ATTEMPTS, OTP_PHONE_BLOCKED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X POST \"https://api.nopexo.com/v1/otp/verify\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"requestId\": \"otp_5hj3k8m2n9p4q7r1\",\n    \"code\": \"123456\"\n  }'"},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/otp/verify\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"requestId\": \"otp_5hj3k8m2n9p4q7r1\",\n    \"code\": \"123456\"\n  }),\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/otp/verify');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'requestId' => 'otp_5hj3k8m2n9p4q7r1',\n        'code' => '123456',\n    ]),\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->post('https://api.nopexo.com/v1/otp/verify', [\n        'requestId' => 'otp_5hj3k8m2n9p4q7r1',\n        'code' => '123456',\n    ]);\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://api.nopexo.com/v1/otp/verify\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    json={\n        \"requestId\": \"otp_5hj3k8m2n9p4q7r1\",\n        \"code\": \"123456\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/otp/resend":{"post":{"operationId":"resendOtp","tags":["OTP"],"summary":"Resend a code","description":"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.\n\nRequired permission: `otp:send`.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResendOtpRequest"},"example":{"requestId":"otp_5hj3k8m2n9p4q7r1"}}}},"responses":{"202":{"description":"Accepted (queued)","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpRequest"},"example":{"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"}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"INSUFFICIENT_FUNDS","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"OTP_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"OTP_ALREADY_VERIFIED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"OTP_EXPIRED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED, OTP_RESEND_COOLDOWN, OTP_RESEND_LIMIT, OTP_PHONE_RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X POST \"https://api.nopexo.com/v1/otp/resend\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"requestId\": \"otp_5hj3k8m2n9p4q7r1\"\n  }'"},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/otp/resend\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"requestId\": \"otp_5hj3k8m2n9p4q7r1\"\n  }),\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/otp/resend');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'POST',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n        'Content-Type: application/json',\n    ],\n    CURLOPT_POSTFIELDS => json_encode([\n        'requestId' => 'otp_5hj3k8m2n9p4q7r1',\n    ]),\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->post('https://api.nopexo.com/v1/otp/resend', [\n        'requestId' => 'otp_5hj3k8m2n9p4q7r1',\n    ]);\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://api.nopexo.com/v1/otp/resend\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    json={\n        \"requestId\": \"otp_5hj3k8m2n9p4q7r1\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}},"/v1/otp/{requestId}":{"get":{"operationId":"getOtp","tags":["OTP"],"summary":"Get a verification","description":"Status of a verification (never the code): pending, verified, expired, failed or cancelled, with attempts and sends left and the SMS delivery status.\n\nRequired permission: `otp:verify`.","parameters":[{"name":"requestId","in":"path","required":true,"description":"The `requestId` from `/v1/otp/send`.","schema":{"type":"string"},"example":"otp_5hj3k8m2n9p4q7r1"}],"responses":{"200":{"description":"OK","headers":{"X-Request-Id":{"description":"Trace ID for this request.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OtpRequest"},"example":{"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}}}},"401":{"description":"INVALID_API_KEY","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"INSUFFICIENT_SCOPE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"OTP_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"RATE_LIMITED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-codeSamples":[{"lang":"cURL","source":"curl -X GET \"https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1\" \\\n  -H \"Authorization: Bearer $NOPEXO_API_KEY\""},{"lang":"Node.js","source":"// Node.js 18+ (built-in fetch)\nconst res = await fetch(\"https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1\", {\n  method: \"GET\",\n  headers: {\n    Authorization: `Bearer ${process.env.NOPEXO_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${data.code}: ${data.message} (trace ${data.traceId})`);\nconsole.log(data);"},{"lang":"PHP","source":"<?php\n$ch = curl_init('https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'GET',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('NOPEXO_API_KEY'),\n    ],\n]);\n$data = json_decode(curl_exec($ch), true);\n$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);\ncurl_close($ch);\nif ($status >= 400) {\n    throw new Exception(\"{$data['code']}: {$data['message']} (trace {$data['traceId']})\");\n}\nprint_r($data);"},{"lang":"Laravel","source":"use Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Str;\n\n// config/services.php: 'nopexo' => ['key' => env('NOPEXO_API_KEY')]\n$response = Http::withToken(config('services.nopexo.key'))\n    ->acceptJson()\n    ->get('https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1');\n\nif ($response->failed()) {\n    throw new \\RuntimeException($response['code'] . ': ' . $response['message'] . ' (trace ' . $response['traceId'] . ')');\n}\n$data = $response->json();"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://api.nopexo.com/v1/otp/otp_5hj3k8m2n9p4q7r1\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['NOPEXO_API_KEY']}\",\n    },\n    timeout=15,\n)\ndata = res.json()\nif not res.ok:\n    raise RuntimeError(f\"{data['code']}: {data['message']} (trace {data['traceId']})\")\nprint(data)"}]}}},"webhooks":{"nopexoEvent":{"post":{"summary":"Nopexo event","description":"Sent to your webhook URL. Verify `Nopexo-Signature` before trusting it. Respond 2xx within 10 seconds; anything else is retried with backoff.","parameters":[{"name":"Nopexo-Signature","in":"header","required":true,"schema":{"type":"string"},"example":"t=1762596900,v1=5f2b…"},{"name":"Nopexo-Event-Id","in":"header","required":true,"schema":{"type":"string"}},{"name":"Nopexo-Event-Type","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"200":{"description":"Received"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"nxk_test_… / nxk_live_…","description":"Project API key from Developers → API keys."}},"schemas":{"Money":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"]},"Error":{"type":"object","required":["code","message","traceId"],"properties":{"code":{"type":"string","description":"Stable machine-readable code. See Error codes.","example":"VALIDATION_FAILED"},"message":{"type":"string","description":"Human-readable explanation, safe to log."},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}}},"description":"Per-field problems (validation errors)."},"retryAfterSeconds":{"type":"integer","description":"When to retry (rate limits and cooldowns)."},"attemptsRemaining":{"type":"integer","description":"OTP attempts left (verification errors)."},"requiredScope":{"type":"string","description":"Missing permission (INSUFFICIENT_SCOPE)."},"traceId":{"type":"string","description":"Also in the X-Request-Id header. Quote it to support.","example":"req_9f2k4m6n8p1q3r5t"}}},"SendSmsRequest":{"type":"object","required":["to","from","body"],"properties":{"to":{"type":"string","description":"Ghana mobile number, local (024…) or international (+233…).","example":"0241234567"},"from":{"type":"string","description":"Sender ID (up to 11 letters/numbers). Production needs an approved one; sandbox accepts any valid name.","example":"NOPEXO"},"body":{"type":"string","description":"Up to 6 SMS parts (918 GSM-7 or 402 Unicode characters).","maxLength":2000}}},"BulkSmsRequest":{"type":"object","required":["from","body"],"properties":{"from":{"type":"string","example":"NOPEXO"},"body":{"type":"string","description":"May use {{field}} placeholders filled from each recipient's fields; {{field|fallback}} sets a default."},"name":{"type":"string","description":"Shown in the dashboard. Optional."},"to":{"type":"array","items":{"type":"string"},"maxItems":10000,"description":"Numbers to send to (no personalization)."},"recipients":{"type":"array","maxItems":10000,"items":{"type":"object","required":["to"],"properties":{"to":{"type":"string"},"fields":{"type":"object","additionalProperties":{"type":"string"}}}},"description":"Numbers with personalization fields. Use `to`, `recipients` or both."},"scheduledAt":{"type":"string","format":"date-time","description":"Send later (1 minute to 90 days ahead). Omit to send now."}}},"Message":{"type":"object","properties":{"id":{"type":"string","example":"msg_7kd2qm4x9t3c8f1v"},"to":{"type":"string","description":"E.164. Masked for OTP messages."},"network":{"type":"string","nullable":true,"enum":["MTN","Telecel","AT",null]},"from":{"type":"string"},"body":{"type":"string","description":"For OTP messages the code is masked."},"encoding":{"type":"string","enum":["GSM-7","UCS-2"]},"segments":{"type":"integer","description":"SMS parts billed."},"status":{"type":"string","enum":["queued","sending","unknown","submitted","delivered","undelivered","expired","rejected","not_sent","cancelled"]},"final":{"type":"boolean","description":"True once the status won't change."},"type":{"type":"string","enum":["single","bulk","otp"]},"environment":{"type":"string","enum":["sandbox","production"]},"simulated":{"type":"boolean","description":"Delivered by Nopexo's simulator (sandbox or test mode), not a real network."},"charge":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"],"nullable":true},"batchId":{"type":"string","nullable":true},"otpRequestId":{"type":"string","nullable":true},"error":{"type":"object","nullable":true,"properties":{"code":{"type":"string"},"message":{"type":"string"}}},"createdAt":{"type":"string","format":"date-time"},"submittedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"Batch":{"type":"object","properties":{"batchId":{"type":"string"},"name":{"type":"string"},"from":{"type":"string"},"status":{"type":"string","enum":["scheduled","queued","sending","completed","cancelled","failed"]},"environment":{"type":"string","enum":["sandbox","production"]},"scheduledAt":{"type":"string","format":"date-time","nullable":true},"recipients":{"type":"object","properties":{"total":{"type":"integer"},"accepted":{"type":"integer"},"skipped":{"type":"integer"}}},"skipped":{"type":"array","items":{"type":"object","properties":{"reason":{"type":"string","enum":["invalid","duplicate","suppressed","missing_data","no_funds"]},"count":{"type":"integer"}}}},"messages":{"type":"object","properties":{"queued":{"type":"integer"},"submitted":{"type":"integer"},"delivered":{"type":"integer"},"failed":{"type":"integer"},"cancelled":{"type":"integer"}}},"estimatedSegments":{"type":"integer"},"estimatedCost":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"]},"failureReason":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"Report":{"type":"object","properties":{"period":{"type":"object","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"}}},"summary":{"type":"object","properties":{"total":{"type":"integer"},"delivered":{"type":"integer"},"pending":{"type":"integer"},"failed":{"type":"integer"},"cancelled":{"type":"integer"},"deliveryRate":{"type":"number"},"segments":{"type":"integer"},"cost":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"]}}},"items":{"type":"array","items":{"$ref":"#/components/schemas/Message"}},"nextCursor":{"type":"string","nullable":true}}},"Balance":{"type":"object","properties":{"currency":{"type":"string","enum":["GHS"]},"available":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"]},"reserved":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"]},"total":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"]},"lowBalance":{"type":"boolean"},"environment":{"type":"string"},"sandbox":{"type":"boolean"},"note":{"type":"string","nullable":true}}},"SendOtpRequest":{"type":"object","required":["to"],"properties":{"to":{"type":"string","example":"0241234567"},"purpose":{"type":"string","description":"Your label, e.g. login, signup, payment (shown in analytics).","default":"verification"},"reference":{"type":"string","description":"Your own reference (e.g. user ID), returned with the verification and in webhooks.","maxLength":100},"length":{"type":"integer","minimum":4,"maximum":10,"description":"Digits. Default from Verify settings (6)."},"expiresIn":{"type":"integer","minimum":60,"maximum":1800,"description":"Seconds. Default from Verify settings (300)."},"sender":{"type":"string","description":"Sender ID for this code. Default from Verify settings."}}},"VerifyOtpRequest":{"type":"object","required":["requestId","code"],"properties":{"requestId":{"type":"string"},"code":{"type":"string","pattern":"^\\d{4,10}$"}}},"ResendOtpRequest":{"type":"object","required":["requestId"],"properties":{"requestId":{"type":"string"}}},"OtpRequest":{"type":"object","properties":{"requestId":{"type":"string","example":"otp_5hj3k8m2n9p4q7r1"},"status":{"type":"string","enum":["pending","verified","expired","failed","cancelled"]},"verified":{"type":"boolean"},"to":{"type":"string","description":"Always masked.","example":"+233 24 ••• 4567"},"network":{"type":"string","nullable":true},"purpose":{"type":"string"},"reference":{"type":"string","nullable":true},"environment":{"type":"string","enum":["sandbox","production"]},"expiresAt":{"type":"string","format":"date-time"},"attemptsRemaining":{"type":"integer"},"sendsRemaining":{"type":"integer"},"resendAvailableAt":{"type":"string","format":"date-time","nullable":true},"delivery":{"type":"object","nullable":true,"properties":{"status":{"type":"string"},"errorCode":{"type":"string","nullable":true}}},"charge":{"type":"object","properties":{"amount":{"type":"string","description":"Exact decimal, 4 places","example":"0.0350"},"currency":{"type":"string","enum":["GHS"]}},"required":["amount","currency"],"nullable":true},"createdAt":{"type":"string","format":"date-time"},"verifiedAt":{"type":"string","format":"date-time","nullable":true},"testCode":{"type":"string","description":"Sandbox only: the code (sandbox never reaches a phone). Never present for production keys."}}},"WebhookEvent":{"type":"object","properties":{"id":{"type":"string","description":"Event ID (same across retries — dedupe on it).","example":"wev_4k8m2n6p9q3r7t1v"},"type":{"type":"string","enum":["message.submitted","message.delivered","message.failed","otp.verified","otp.failed","bulk.completed","webhook.test"]},"createdAt":{"type":"string","format":"date-time"},"environment":{"type":"string","enum":["sandbox","production"]},"projectId":{"type":"string"},"data":{"type":"object","description":"A Message, OtpRequest or Batch, depending on `type`."}}}}}}