API reference
Every request below is live against https://api.ipl.com.ng. Payloads and responses are taken from the running platform, not written from memory.
Authentication
Create a key in the client portal under API Credentials. Keys begin with ipl_ and are shown once — store it immediately. Send it as a Bearer token on every request.
Authorization: Bearer ipl_your_api_key Accept: application/json Content-Type: application/json
401 with {"message":"Unauthenticated."}. Keys are hashed at rest, so a lost key cannot be recovered — revoke it and create another.https://api.ipl.com.ng/api/client/sms/sendSend a single message
Queues one message and debits your wallet in the same transaction. The response carries the billed segment count and your resulting balance.
Request body
| Field | Type | Notes |
|---|---|---|
recipient | string | Required. 08031234567 or +2348031234567 — normalised to E.164. |
sender_id | string | Required, max 11 chars. Must be an approved sender ID. |
message | string | Required. Segmented at 160 / 153 characters. |
200 — response
{
"message": "SMS queued for sending",
"data": {
"message_id": 27039,
"segments": 1,
"cost": 5.85,
"status": "queued",
"new_balance": "994.15"
}
}422 — recipient opted out
{
"success": false,
"message": "Recipient has opted out of receiving messages",
"error_code": "RECIPIENT_OPTED_OUT"
}https://api.ipl.com.ng/api/client/sms/status/{id}Check a message
Returns the full record, including the operator's message id and delivery timestamps.
Statuses
| queued | Accepted and billed, awaiting a worker. |
| sent | Handed to the operator; awaiting a receipt. |
| delivered | Confirmed on the handset. |
| failed | Rejected or undeliverable — automatically refunded. |
200 — response
{
"data": {
"id": 27039,
"recipient_phone": "+2347031857635",
"sender_id_value": "IPLSMS",
"segments_count": 1,
"cost": "5.85",
"status": "delivered",
"is_sandbox": false,
"jasmin_message_id": "29973d46-1fdd-4956-91d5-c54efbf8510d",
"failure_reason": null,
"sent_at": "2026-09-08T22:02:08.000000Z",
"delivered_at": "2026-09-08T22:02:16.000000Z"
}
}https://api.ipl.com.ng/api/client/balanceWallet balance
Your spendable balance and its currency.
200 — response
{
"balance": 88.3,
"currency": "NGN"
}Related
GET | /api/client/wallet/transactions |
POST | /api/client/wallet/fund/initialize |
GET | /api/client/wallet/fund/verify/{reference} |
https://api.ipl.com.ng/api/client/sender-id/approvedApproved sender IDs
Only approved IDs can be used to send. Global IDs are available to every account.
200 — response
{
"success": true,
"data": [
{ "id": 1, "sender_name": "IPLSMS", "user_id": null, "is_global": true }
]
}Request a new ID
POST /api/client/sender-id/request { "sender_name": "ACME" }
Sender IDs are reviewed before approval. Max 11 characters.
https://api.ipl.com.ng/api/client/bulk-smsCreate a bulk campaign
Campaigns are prepared asynchronously — recipients are expanded, the wallet is debited and messages are queued by a background job, so a large campaign never blocks the request.
Request body
| Field | Notes |
|---|---|
name | Required. Campaign label. |
sender_id | Required. Approved ID or its numeric id. |
message_text | Required, max 1000 chars. |
recipient_type | Required: all, contacts, groups or manual. |
contact_ids | Array, required when type is contacts. |
group_ids | Array, required when type is groups. |
manual_numbers | Newline or comma separated, for manual. |
scheduled_at | Optional ISO datetime, must be in the future. |
200 — response
{
"success": true,
"message": "Campaign queued for processing.",
"data": {
"id": 13,
"name": "March promo",
"status": "draft",
"scheduled_at": null
}
}Poll GET /api/client/bulk-sms/{id} for progress. If the wallet cannot cover the campaign, no messages are created and nothing is billed.
https://api.ipl.com.ng/api/client/verify/startSend an OTP
Generates and sends a one-time code, then verifies it for you — no need to store codes yourself.
Request body
| Field | Notes |
|---|---|
phone | Required. |
sender_id | Optional, max 11 chars. |
length | Optional, 4–8. Default 6. |
ttl | Optional seconds, 60–3600. |
template | Optional, max 320 chars. |
200 — response
{
"message": "Verification code sent",
"data": {
"verification_id": 1,
"phone": "+2348030000002",
"status": "pending",
"expires_at": "2026-09-09T07:39:30+01:00"
}
}Then check it
POST /api/client/verify/check { "phone": "+2348030000002", "code": "481920" }
https://api.ipl.com.ng/api/client/opt-outsOpt-out list
Numbers on this list are refused at send time with RECIPIENT_OPTED_OUT, before any charge is made.
Add one — 201
{ "phone": "08031234567", "reason": "customer request" }Add many
POST /api/client/opt-outs/bulk { "phones": ["08031234567", "08099998888"] }
List — 200
{
"current_page": 1,
"data": [
{
"id": 1,
"phone_e164": "+2348031234567",
"reason": "customer request",
"source": "manual"
}
]
}Remove with DELETE /api/client/opt-outs/{id}.
https://api.ipl.com.ng/api/client/webhooksDelivery webhooks
Point IPLSMS at your endpoint and every terminal delivery state is pushed to you, signed.
Configure
{
"dlr_url": "https://yourapp.com/webhooks/iplsms",
"is_active": true
}Current config — 200
{
"dlr_url": "https://yourapp.com/webhooks/iplsms",
"is_active": true,
"signing_secret": "whsec_…",
"failed_24h": 0
}What we POST to you
X-IPLSMS-Signature: <hmac-sha256> X-IPLSMS-Event: message.delivered User-Agent: IPLSMS-Webhook/1.0 { "event": "message.delivered", "data": { "message_id": 27039, "recipient": "+2347031857635", "sender_id": "IPLSMS", "status": "delivered", "segments": 1, "cost": 5.85, "failure_reason": null, "is_sandbox": false, "sent_at": "2026-09-08T22:02:08+00:00", "delivered_at": "2026-09-08T22:02:16+00:00" }, "timestamp": "2026-09-08T22:02:16+00:00" }
Verify the signature
// the signature is an HMAC-SHA256 of the raw body hash_hmac('sha256', $rawBody, $signingSecret) === $header;
Events fire on message.delivered and message.failed. Failed deliveries are retried up to 4 times with a 10s / 60s / 300s backoff. Rotate the secret with POST /api/client/webhooks/rotate-secret.
https://api.ipl.com.ng/api/client/sandbox/sendSandbox
Identical request shape to the live endpoint, but nothing is billed and nothing reaches a handset — use it to build and test your integration.
200 — response
{
"message": "Sandbox message accepted (not billed, not delivered)",
"data": {
"message_id": 27041,
"segments": 1,
"status": "sent",
"is_sandbox": true
}
}Simulate a receipt
POST /api/client/sandbox/simulate-dlr { "message_id": 27041, "status": "delivered" }
This fires your webhook exactly as a real receipt would, so you can test signature verification without spending anything. status accepts delivered or failed.
SMPP binds
For sustained throughput, bind directly over SMPP 3.4. SMPP is provisioned per account rather than open by default — request access from the client portal under API Credentials, and the host, port and system_id are issued once your source IP is whitelisted.
| Setting | Value |
|---|---|
| Host | Issued with your credentials |
| Port | 2775 |
| Bind type | transceiver |
| Version | SMPP 3.4 |
| system_id | Issued with your credentials |
Before you bind
- · Register
registered_delivery = 1to receive receipts. - · Messages are billed per segment on submit, identically to the HTTP API.
- · Your source IP must be whitelisted — send it with your access request.
- · Use a single transceiver bind and let it persist; reconnect with backoff.
Errors
Validation failures return 422 with a field-keyed errors object, in standard Laravel shape.
| Code | Meaning |
|---|---|
200 | Accepted. |
201 | Created. |
401 | Missing, revoked or unknown API key. |
402 | Insufficient wallet balance. |
404 | Record not found or not yours. |
422 | Validation failed, or recipient opted out. |
429 | Rate limited — back off and retry. |
422 — validation
{
"message": "The recipient field is required. (and 2 more errors)",
"errors": {
"recipient": ["The recipient field is required."],
"sender_id": ["The sender id field is required."],
"message": ["The message field is required."]
}
}