Skip to content
Reference

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
A revoked or unknown key returns 401 with {"message":"Unauthenticated."}. Keys are hashed at rest, so a lost key cannot be recovered — revoke it and create another.
POSThttps://api.ipl.com.ng/api/client/sms/send

Send 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

FieldTypeNotes
recipientstringRequired. 08031234567 or +2348031234567 — normalised to E.164.
sender_idstringRequired, max 11 chars. Must be an approved sender ID.
messagestringRequired. 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"
}
GEThttps://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

queuedAccepted and billed, awaiting a worker.
sentHanded to the operator; awaiting a receipt.
deliveredConfirmed on the handset.
failedRejected 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"
  }
}
GEThttps://api.ipl.com.ng/api/client/balance

Wallet 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}
GEThttps://api.ipl.com.ng/api/client/sender-id/approved

Approved 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.

POSThttps://api.ipl.com.ng/api/client/bulk-sms

Create 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

FieldNotes
nameRequired. Campaign label.
sender_idRequired. Approved ID or its numeric id.
message_textRequired, max 1000 chars.
recipient_typeRequired: all, contacts, groups or manual.
contact_idsArray, required when type is contacts.
group_idsArray, required when type is groups.
manual_numbersNewline or comma separated, for manual.
scheduled_atOptional 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.

POSThttps://api.ipl.com.ng/api/client/verify/start

Send an OTP

Generates and sends a one-time code, then verifies it for you — no need to store codes yourself.

Request body

FieldNotes
phoneRequired.
sender_idOptional, max 11 chars.
lengthOptional, 4–8. Default 6.
ttlOptional seconds, 60–3600.
templateOptional, 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" }
POSThttps://api.ipl.com.ng/api/client/opt-outs

Opt-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}.

PUThttps://api.ipl.com.ng/api/client/webhooks

Delivery 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.

POSThttps://api.ipl.com.ng/api/client/sandbox/send

Sandbox

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.

SettingValue
HostIssued with your credentials
Port2775
Bind typetransceiver
VersionSMPP 3.4
system_idIssued with your credentials

Before you bind

  • · Register registered_delivery = 1 to 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.

CodeMeaning
200Accepted.
201Created.
401Missing, revoked or unknown API key.
402Insufficient wallet balance.
404Record not found or not yours.
422Validation failed, or recipient opted out.
429Rate 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."]
  }
}