Skip to content

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-key: <IP_AGENT_KEY>
x-user-phone-number: 08012345678
  • x-agent-key is compared in constant time against process.env.IP_AGENT_KEY. A mismatch returns 401 with { "error": [{ "msg": "Unauthorized", "path": "agent" }] }.
  • x-user-phone-number resolves the customer. A missing header returns 401 with "No phone Id provided"; an unknown number returns 401 with "No user found".

The request then runs with that customer's identity:

req.user = user;
req.userDomain = 'live';
req.userAuth = 'whatsapp-agent';

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:

{ "error": [{ "msg": "Use the app or website for wallet transfers and balance moves" }] }

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