Agent Channel (WhatsApp)¶
The /api/v1/agent/* routes are the server-to-server surface used by the
InstaPayBill WhatsApp assistant (Halo). They are not a general-purpose
integration API — they authenticate a server with a shared secret and act on
behalf of a customer identified by phone number.
Not a general integration surface
These routes bypass the normal per-user API key model: a single shared secret authorises the caller, and the customer is identified by a header. If you are building a product integration, use the standard API-key endpoints instead.
Authentication¶
Two headers are required on every request:
x-agent-keyis compared in constant time againstprocess.env.IP_AGENT_KEY. A mismatch returns401with{ "error": [{ "msg": "Unauthorized", "path": "agent" }] }.x-user-phone-numberresolves the customer. A missing header returns401with "No phone Id provided"; an unknown number returns401with "No user found".
The request then runs with that customer's identity:
The identity is server-established
The whatsapp-agent channel is set by the server after it has resolved the
customer — it is never asserted by a client header. This is why the
channel skips the wallet PIN check and why the daily purchase cap is
enforced per phone number.
Endpoint map¶
Customers¶
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/agent/auth |
Agent login. Returns { data: { token } }. |
GET |
/api/v1/agent/user?phoneNumber=… |
Customer lookup |
GET |
/api/v1/agent/receipt |
Branded receipt PDF |
GET /api/v1/agent/user returns the customer profile plus the flags a buying
assistant needs:
{
"data": {
"user": { "email": "…", "telephone": "08012345678", "privilege": "user" },
"isPinSet": true,
"hasKYC": true,
"isEmailVerified": true,
"isSuspended": false,
"wallet": { "available": 5000, "commission": 0, "bonus": 0 },
"lastTransaction": { }
}
}
Purchases¶
All require x-agent-key, x-user-phone-number, a completed KYC, an active
account, and a valid frontend-source channel.
| Method | Path | Body |
|---|---|---|
POST |
/api/v1/agent/airtime/buy |
amount, network, phone |
POST |
/api/v1/agent/data/buy |
planId, phone |
POST |
/api/v1/agent/electricity/buy |
type, disco, amount, meterNo, phoneNumber |
POST |
/api/v1/agent/electricity/validate |
meterNo, disco, type |
POST |
/api/v1/agent/cable/buy |
smartCardNo, type |
POST |
/api/v1/agent/cable/validate |
smartCardNo, type |
POST |
/api/v1/agent/education/buy |
numberOfPin, type |
POST |
/api/v1/agent/biz/verify |
verifyType + name or biz_id |
These call the same services as the public API, so the response envelope and
the code semantics are identical — see Airtime for
a worked example.
Wallet¶
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/agent/wallet/recipient-lookup |
Resolve a transfer recipient. q + type (telephone, wallet, email). |
Wallet transfers are blocked on this channel
A router-level guard rejects every POST under /api/v1/agent/wallet/*
with HTTP 403:
This means POST /api/v1/agent/wallet/transfer,
/move-commission, /move-bonus and /move-referral are unreachable,
even though they are declared in the route file. Only the GET recipient
lookup works. Use the app or website for balance moves.
Verification and notifications¶
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/agent/biz/verify |
Business name or RC/BN verification |
POST |
/api/v1/agent/security-alert |
Email a security alert to the customer. Body: phoneNumber, event, detail. |
POST |
/api/v1/agent/otp/send |
Send a purchase OTP to the customer's WhatsApp. Body: phoneNumber. |
POST /api/v1/agent/otp/send returns the OTP in the response body when the
request is agent-authenticated — which is what makes the WhatsApp purchase
flow work without asking the customer to type a code they received by SMS.
Channel-specific rules¶
Wallet PIN is skipped¶
The wallet PIN check is bypassed for agent-authenticated requests. The WhatsApp flow uses a single-use OTP instead, with a 10-minute expiry.
Promotional codes are not applied¶
applyPromoToPurchase is skipped when the channel is api or
whatsapp-agent. Promotional pricing does not apply on this channel.
Daily purchase cap¶
A per-phone-number cap of ₦100,000/day applies, summed from transactions
on the whatsapp-agent channel with code in {200, 300}.
Pilot mode¶
When usecases.whatsappPilotMode is enabled, purchases are restricted to the
phone numbers in usecases.whatsappPilotPhones.
Global pause¶
usecases.whatsappBuysEnabled must be enable or the buy routes return
300 with "WhatsApp purchases are briefly paused for maintenance…" and
path: "whatsapp_paused". This guard fails open on a database error.
OTP rate limit¶
POST /api/v1/agent/otp/send is capped at 5 requests per 10 minutes per
phone number, keyed on the phone number rather than the IP — every request
comes from the same Halo server, so an IP key would throttle all customers
together.
Middleware order¶
requireAgentApiKey → 401 if x-agent-key is wrong
attachAgentPhoneUser → 401 if the phone header is missing or unknown
checkNetworkState → 300 + canSchedule if the network is degraded
RequireAgentKYC → 300 if the customer has no BVN and no NIN
UserActive → 403 if the account is suspended
UserAuthSouceNecessary → 300 if the channel header is invalid
UserAuthSouceNecessaryChecker → 300 if the channel is switched off
MaximumLimiterPurchase → 429 on excess
express-validator → 300 on a bad field
Related¶
- Authentication — the credential model
- Errors — the error contract
- MCP — the tool surface for AI agents