KulPayDOCS Live

For PSPs and banks

Integrating with KulPay as a PSP

You serve customers: in an app, a wallet, USSD, a branch or an agent network. KulPay gives you payment rails, cash deposits and every biller on the platform (electricity, water, TV, telecoms, schools) behind one API, one key and one set of webhooks. This guide takes you from applying to selling your first bill.

What a PSP does on KulPay

You want toUse
Accept or originate payments (wallets, cards, references)Payments: charges, refunds, webhooks
Take cash for KulPay accounts at your branches or agentsCash deposits
Sell bills, top-ups, tokens and vouchers to your customersBill payments (this guide)
Show your customers the bills their billers send themPayers and subscriptions

In every bill payment you are the initiating_psp: the channel the payer paid through (see Participants).

From application to live

  1. Apply at /onboarding?kind=psp: company, licence, roles, transaction types, countries and currencies, volumes, technical and compliance contacts. You can save a draft and come back.
  2. KYB review. KulPay's compliance team reviews your documents and screenings and sets a risk tier. Approval is maker-checker: the officer who prepares it cannot approve it.
  3. Commercial setup. The contract and your fee schedule.
  4. Technical setup, in the sandbox. A test key is issued, and you complete seven steps in the portal, each validated as you save it:
    • the capability matrix (what you support: transaction types, methods);
    • your orientation profile;
    • your endpoints (HTTPS, public, TLS 1.2+, a certificate not about to expire);
    • credentials (mTLS client certificate, PGP key for statements);
    • webhook endpoints;
    • the IP allowlist;
    • the statement profile.
  5. Certification. A suite generated from your capability matrix: one case per enabled transaction type, method and outcome, plus callbacks, retries, idempotent replay, signature checks and a sample statement. You get a signed report.
  6. Pilot. Your live key is issued (shown once in the portal). Limits are 10% of your contracted limits by default, with a merchant allow-list and a routing weight.
  7. Live after 7 days of pilot with no critical incident and matched reconciliation, approved by two people. Live keys and live webhooks are then available in the portal.

The portal's home page shows where you are and the next action. Your logo (Perfil): a PNG with a transparent background, 256 to 2048 px a side, at most 1 MB, reviewed before it is published to channels.

Keys, scopes and modes

Create keys on the portal's Integração page and send one on every call:

Authorization: Bearer YOUR_API_KEY

Every scope a partner key can hold and the operations it opens, read from the gateway's routes:

ScopeAllowsOperations
bills:read List billers and products, and look up customer references.
12 operation(s)
  • GET /v1/bill-commissions
  • POST /v1/bill-invoices/lookups
  • GET /v1/bill-invoices/{id}/document
  • GET /v1/bill-payments
  • POST /v1/bill-payments/lookups
  • GET /v1/bill-payments/{id}
  • GET /v1/bill-payments/{id}/receipt.pdf
  • GET /v1/billers
  • GET /v1/billers/{code}
  • GET /v1/payers/{id}
  • GET /v1/payers/{id}/invoices
  • GET /v1/payers/{id}/subscriptions
bills:write Quote and pay bills, airtime and vouchers.
7 operation(s)
  • POST /v1/bill-invoices/{id}/engagements
  • POST /v1/bill-payments
  • POST /v1/bill-payments/quotes
  • POST /v1/bill-payments/{id}/resend
  • POST /v1/payers
  • POST /v1/payers/{id}/subscriptions
  • DELETE /v1/payers/{id}/subscriptions/{sid}
events:read Read the account's events and catch up on webhooks.
3 operation(s)
  • GET /v1/event-types (or webhooks:manage)
  • GET /v1/events
  • GET /v1/events/{id}
invoices:read A biller reads the invoices it presented.
8 operation(s)
  • GET /v1/biller/engagement-digest
  • GET /v1/biller/invoices
  • GET /v1/biller/invoices/{number}
  • GET /v1/biller/invoices/{number}/document
  • GET /v1/biller/invoices/{number}/events
  • GET /v1/biller/remittances
  • GET /v1/biller/remittances/{id}/report.csv
  • GET /v1/biller/reports/daily
invoices:write A biller presents, changes and cancels its invoices.
7 operation(s)
  • PUT /v1/biller/engagement-digest
  • POST /v1/biller/invoice-files
  • POST /v1/biller/invoices
  • PATCH /v1/biller/invoices/{number}
  • DELETE /v1/biller/invoices/{number}/document
  • PUT /v1/biller/invoices/{number}/document
  • POST /v1/biller/reports/daily
keys:manage Issue, list and revoke the account's own API keys.
14 operation(s)
  • GET /v1/api-keys (or admin:read)
  • POST /v1/api-keys (or admin:write)
  • DELETE /v1/api-keys/{id} (or admin:write)
  • PATCH /v1/api-keys/{id} (or admin:write)
  • POST /v1/api-keys/{id}/roll (or admin:write)
  • GET /v1/api-keys/{id}/usage (or admin:read)
  • GET /v1/client-ca (or admin:read)
  • GET /v1/client-certificates (or admin:read)
  • POST /v1/client-certificates (or admin:write)
  • DELETE /v1/client-certificates/{id} (or admin:write)
  • PUT /v1/mtls (or admin:write)
  • GET /v1/oauth-clients (or admin:read)
  • POST /v1/oauth-clients (or admin:write)
  • DELETE /v1/oauth-clients/{id} (or admin:write)
payers:write A biller enrols its customers to receive its bills on KulPay channels.
5 operation(s)
  • GET /v1/biller/notices
  • PUT /v1/biller/notices
  • GET /v1/biller/payers
  • POST /v1/biller/payers
  • DELETE /v1/biller/payers/{ref}
payments:capture Capture authorised payments.
1 operation(s)
  • POST /charges/{id}/capture
payments:read Read payments and their receipts.
9 operation(s)
  • GET /files
  • GET /files/{id}
  • GET /files/{id}/content
  • GET /methods
  • GET /payments/{id}/files
  • GET /payments/{provider_payment_id}
  • GET /transactions
  • GET /v1/disputes
  • GET /v1/disputes/{id}
payments:refund Refund payments.
1 operation(s)
  • POST /refunds
payments:write Create payments and cancel them.
6 operation(s)
  • POST /charges
  • POST /charges/{id}/complete-action
  • POST /charges/{id}/void
  • POST /files
  • POST /v1/disputes/{id}/accept
  • POST /v1/disputes/{id}/evidence
payouts:approve Approve or reject payouts above the approval threshold.
2 operation(s)
  • POST /v1/payouts/{id}/approve
  • POST /v1/payouts/{id}/reject
payouts:read Read payouts and the available balance.
4 operation(s)
  • GET /payouts/{provider_payout_id}
  • GET /v1/balance
  • GET /v1/payouts
  • GET /v1/payouts/{id}
payouts:write Send payouts and look up beneficiary names.
4 operation(s)
  • POST /payouts
  • POST /v1/name-lookups (or transactions:write)
  • POST /v1/name-lookups/{lookup_id}/select (or transactions:write)
  • POST /v1/payouts
psp:configure Configure the PSP's own capability matrix and orientation.
16 operation(s)
  • GET /v1/psps/{id}/capabilities
  • PUT /v1/psps/{id}/capabilities
  • GET /v1/psps/{id}/commissions
  • POST /v1/psps/{id}/credit-reconciliations
  • GET /v1/psps/{id}/credit-report
  • GET /v1/psps/{id}/keys
  • POST /v1/psps/{id}/keys
  • DELETE /v1/psps/{id}/keys/{key_id}
  • GET /v1/psps/{id}/orientation
  • PUT /v1/psps/{id}/orientation
  • GET /v1/psps/{id}/retry-policy
  • PUT /v1/psps/{id}/retry-policy
  • GET /v1/psps/{id}/statement-endpoints
  • GET /v1/psps/{id}/statement-profiles
  • PUT /v1/psps/{id}/statement-profiles
  • POST /v1/psps/{id}/statement-profiles/preview
reconciliation:read Read statements, matches and exceptions.
11 operation(s)
  • GET /v1/ledger/accounts
  • GET /v1/ledger/journals
  • GET /v1/ledger/journals/{id}
  • GET /v1/psp-settlements
  • GET /v1/reconciliation/exceptions
  • GET /v1/reconciliation/exceptions/{id}
  • GET /v1/statements
  • GET /v1/statements/{id}
  • GET /v1/statements/{id}/lines
  • GET /v1/statements/{id}/matches
  • GET /v1/statements/{id}/original
reconciliation:write Import statements and resolve exceptions.
8 operation(s)
  • POST /v1/psp-settlements
  • POST /v1/reconciliation/exceptions/{id}/actions
  • POST /v1/reconciliation/exceptions/{id}/assign
  • POST /v1/reconciliation/exceptions/{id}/comments
  • POST /v1/reconciliation/matches
  • POST /v1/statements
  • POST /v1/statements/{id}/match
  • POST /v1/statements/{id}/reparse
settlements:read Read settlement batches and statements.
3 operation(s)
  • GET /v1/settlements
  • GET /v1/settlements/{id}
  • GET /v1/settlements/{id}/report
transactions:read Read a transaction's timeline, stages and reversals.
9 operation(s)
  • GET /v1/cash-deposits
  • GET /v1/cash-deposits/intents
  • GET /v1/cash-deposits/intents/{id}
  • GET /v1/cash-deposits/{id}
  • GET /v1/psp/transactions
  • GET /v1/psp/transactions/{id}
  • GET /v1/transactions/{id}/reversals
  • GET /v1/transactions/{id}/stages
  • GET /v1/transactions/{id}/timeline
transactions:write Reverse transactions.
7 operation(s)
  • POST /v1/cash-deposits
  • POST /v1/cash-deposits/intents
  • POST /v1/cash-deposits/intents/{id}/confirm
  • POST /v1/name-lookups (or payouts:write)
  • POST /v1/name-lookups/{lookup_id}/select (or payouts:write)
  • POST /v1/psp/transactions
  • POST /v1/transactions/{id}/reversals
webhooks:manage Manage webhook endpoints and replay deliveries.
15 operation(s)
  • GET /v1/event-types (or events:read)
  • GET /v1/webhook-endpoints (or admin:read)
  • POST /v1/webhook-endpoints
  • DELETE /v1/webhook-endpoints/{id}
  • GET /v1/webhook-endpoints/{id} (or admin:read)
  • PATCH /v1/webhook-endpoints/{id}
  • PUT /v1/webhook-endpoints/{id}/contacts
  • GET /v1/webhook-endpoints/{id}/deliveries (or admin:read)
  • POST /v1/webhook-endpoints/{id}/enable
  • POST /v1/webhook-endpoints/{id}/pause
  • POST /v1/webhook-endpoints/{id}/roll-secret
  • PUT /v1/webhook-endpoints/{id}/schema-versions
  • PUT /v1/webhook-endpoints/{id}/signing
  • POST /v1/webhook-endpoints/{id}/test
  • GET /v1/webhook-schema-versions (or admin:read)
webhooks:replay Replay webhook deliveries.
2 operation(s)
  • POST /v1/webhook-deliveries/{id}/replay
  • POST /v1/webhook-endpoints/{id}/replay

A key is test or live: test keys run against mock rails and simulated suppliers and move no money. The base URL is https://gateway.kulpay.pavulla.com/v1; bodies are JSON and amounts are decimal strings ("500.00").

Payments and cash deposits

Payments (charges, the hosted checkout, refunds, payment webhooks) are in the integration guide. Cash deposits for KulPay accounts take three calls:

POST https://gateway.kulpay.pavulla.com/v1/name-lookups            {"method": "cash_deposit", "reference": "INV-1001"}
POST https://gateway.kulpay.pavulla.com/v1/cash-deposits/intents   {"lookup_id": "…", "amount": "1500.00", "currency": "MZN"}
POST https://gateway.kulpay.pavulla.com/v1/cash-deposits/intents/{id}/confirm   {"account": "…", "entry_id": "…", "value_date": "2026-10-08"}

A deposit is credited only when the receiving account's statement shows the same entry.

Selling bills — 1. List billers

GET https://gateway.kulpay.pavulla.com/v1/billers?category=electricity
GET https://gateway.kulpay.pavulla.com/v1/billers/{code}

Each biller lists the products you may sell:

Show each biller with its logo_url: a transparent PNG hosted by KulPay at /logos/{id}?v=…, public and cacheable for good, made to sit on your own colours.

2. Look up the reference

curl -X POST https://gateway.kulpay.pavulla.com/v1/bill-payments/lookups \
  -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' \
  -d '{"biller": "EDM", "product": "CREDELEC", "customer_reference": "01123456781"}'

status is found (with the customer's name, masked as the payer should see it, the amount due and the limits) or not_found. Show the name to the payer before they pay. A refused reference (a blocked meter, a closed account) answers 422 reference_rejected with a message for the payer.

If your contract with KulPay allows it, we can switch on full names for your channel: the lookup then answers the customer's name unmasked. It is turned on by two of our operators with your contract's reference, and each such lookup is recorded in our audit trail.

3. Quote

POST https://gateway.kulpay.pavulla.com/v1/bill-payments/quotes  {"biller": "EDM", "product": "CREDELEC", "lookup_id": "…", "amount": "500"}
POST https://gateway.kulpay.pavulla.com/v1/bill-payments/quotes  {"invoice_id": "…"}            # a bill the payer received

The quote fixes amount, payer_fee, total and who bears the fee (fee_bearer); a prepaid quote also says what it buys (breakdown.units). Show the total and pay before quote_expires_at.

4. Pay

curl -X POST https://gateway.kulpay.pavulla.com/v1/bill-payments \
  -H 'Authorization: Bearer YOUR_API_KEY' -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: order-1001' \
  -d '{"quote_id": "…", "funding": "balance", "partner_reference": "order-1001",
       "payer": {"phone": "258841234567"}}'
fundingMoney comes from
balanceYour prefunded balance on KulPay (GET /v1/balance)
collectThe payer, first: add "collect": {"method": "mpesa", "phone": "…"}; the bill is bought once the payer paid, and refunded to them if it cannot be delivered
Always send an Idempotency-Key, and send the same one again if you did not get an answer: you get the same payment back, never a second one. A quote pays once (409 quote_used).

The answer's status:

5. Give the payer what they bought

6. Payers and their bills

Register your customers once, and subscribe them to the billers whose bills they want, with their consent:

POST https://gateway.kulpay.pavulla.com/v1/payers                         {"external_id": "cust-77", "name": "Ana", "phone": "+258841234567"}
POST https://gateway.kulpay.pavulla.com/v1/payers/{id}/subscriptions      {"biller": "AGUAS", "account_reference": "CL-001234",
                                                     "consent": {"accepted": true, "text_version": "v1"}}
GET  https://gateway.kulpay.pavulla.com/v1/payers/{id}/invoices            # bills to show them
DELETE https://gateway.kulpay.pavulla.com/v1/payers/{id}/subscriptions/{sid}   # opt out, at any time

7. Report what payers do

POST https://gateway.kulpay.pavulla.com/v1/bill-invoices/{id}/engagements  {"type": "opened", "payer_id": "cust-77", "surface": "app"}

Report when a payer opens a bill (opened), saves it (pinned) or no longer (unpinned); surface is app, web, ussd or agent. The biller is told of the first open and of pin changes, the payer named only by an opaque reference. Repeats answer recorded: false.

Balance and commissions

GET /v1/balance is your prefunded balance; below the threshold KulPay agrees with you, you get bill_balance.low. GET /v1/bill-commissions is your monthly commission statement.

Webhooks

Register endpoints in the portal; every delivery is signed with KulPay-Signature (verify it), retried on failure, and may repeat or arrive out of order: process each event id once. Catch up after an outage with GET /v1/events?since=.

Each event says its schema_version. To keep receiving a type in the version you built against, pin it on the endpoint: PUT /v1/webhook-endpoints/{id}/schema-versions with {"schema_versions": {"bill_payment.succeeded": 2}}. We deliver it in that version for as long as it is supported and email the endpoint's contacts 90 days before we retire one; GET /v1/webhook-schema-versions lists the versions and their retirement dates.

EventWhat to do
bill_payment.succeededGive the payer the token, PIN or receipt (read them from the payment). Carries participants.
bill_payment.failedTell the payer it failed; nothing was charged, or it was refunded.
bill_payment.pendingTell the payer it is being completed.
bill_payment.refundedThe collected payer was refunded.
bill_payment.reversedA succeeded invoice payment was reversed by operators with four eyes (its money never reached the biller, or the payer was refunded): the total is back in your balance and the invoice can be paid again. A collected payer is refunded (bill_payment.refunded follows); when you funded it from your balance, refund your customer.
bill_invoice.delivered / bill_invoice.remindedShow the payer the new bill, or remind them.
bill_subscription.opted_in / …opted_out / …endedKeep the payer's list of billers in step.
bill_balance.lowTop up your balance.
partner.alertAn operational alert, also emailed to your contacts: an API key or certificate expiring, a key found in public, a statement not received. Route it to your on-call.
payment.*, refund.*, cash_in.*See the integration guide.

Every schema is in the event catalogue.

Participants in a payment

RoleWho
initiating_pspYou: the channel the payer paid through
from_pspWhere the money came from: your prefunded balance (prefunded_balance), or the payer's wallet or bank when collected (collected_from_payer, with the method)
to_pspWhere it goes for the biller: its bank, by remittance; or KulPay, for products bought from a supplier against its float

They are on GET /v1/bill-payments/{id} and on bill_payment.succeeded.

Testing

With a test key, against the sandbox billers:

Biller / productReferenceResult
EDM_SANDBOX / CREDELEC01123456781Token; 20% of the purchase pays a 250.00 debt
01123456782Token for the whole amount
01123456783Blocked meter: reference_rejected
01123456784pending, then succeeded by query
REF_SANDBOXending 0000 / 9991 / 9992 / 9993 / 9995not found / refused / pending then succeeded / out of float / server error then succeeded

Collect-first in test mode takes "collect": {"method": "mpesa", "scenario": "succeed"} or "fail". The developer console's Bills & billers workspace runs every call above and shows each request and answer.

Go-live checklist

Errors

Errors are RFC 9457 problem documents (application/problem+json) with a stable code; branch on it, the detail is for people. Every code, and what to do about it, is in the error catalogue:

StatusCodeMeans
400invalid_requestThe request is wrong; message says how
401No key, or an unknown one
403insufficient_scopeThe key lacks the scope (missing_scope)
404not_foundNo such biller, product, payment, payer or document of yours
409quote_used, invoice_closedThe quote was paid; the bill is no longer payable
422insufficient_balance, amount_out_of_range, reference_rejected, consent_requiredNot enough balance; an amount outside the product's limits; the supplier refused the reference; consent missing
422biller_unavailableThe biller is not accepting payments (suspended or not yet live). Retrying will not help; offer the payer something else.
503biller_unavailableThe biller's supplier is down or not answering. It passes: retry after Retry-After seconds.

Machine-readable reference: OpenAPI 3.1 spec and the event catalogue. Billers integrate with the biller guide.