API reference
Bill payments
Billers, products and supply connectors (epics 14–19): prepaid electricity, top-ups, airtime and data, postpaid bills and vouchers.
GET /pay/invoice/{token}
An invoice's payment page
Public, no key: the token in the invoice's pay_url is the capability (BIL-5.9). Shows the biller, the invoice, the open amount and the payer's fee, and the RF reference to pay at a partner bank. 404 for an unknown or reissued token.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
token required | path | string |
Responses
- 200 The HTML page.
- 404 No such link: the unavailable-link page.
Request
curl -X GET "$KULPAY_URL/pay/invoice/{token}" \
-H "Authorization: Bearer $KULPAY_KEY"
POST /pay/invoice/{token}
Pay an invoice from its page
Form post (with the page's CSRF token) of the open amount, or amount when the biller takes partial payments: KulPay quotes it, collecting on the biller's behalf on its platform account, and redirects (303) to the hosted checkout for the total, where the payer chooses M-Pesa, e-Mola, a card and so on. Once the checkout payment succeeds the invoice is paid (funding collect) and the biller is told with bill_invoice.payment_received.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
token required | path | string |
Request body application/x-www-form-urlencoded
| Field | Type | Description |
|---|---|---|
_csrf | string | |
amount | string |
Responses
- 303 To /checkout/{id}.
- 404 No such link: the unavailable-link page.
- 422 The amount is not accepted, or the invoice takes no more payments: the page again, with the reason.
Request
curl -X POST "$KULPAY_URL/pay/invoice/{token}" \
-H "Authorization: Bearer $KULPAY_KEY"
GET /pay/invoice/{token}/qr.svg
The payment page's QR
Public, as the payment page: the invoice's QR as SVG, for a payer who opened the page on a computer and pays with the app on their phone (BIL-5.12).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
token required | path | string |
Responses
- 200 The QR.
- 404 No such link, or the invoice takes no payments.
Request
curl -X GET "$KULPAY_URL/pay/invoice/{token}/qr.svg" \
-H "Authorization: Bearer $KULPAY_KEY"
PUT /v1/admin/accounts/{id}/bill-balance-alert
A channel's low-balance alert
The balance below which a channel paying bills from its balance is sent bill_balance.low (once, until it is back above). low_balance_minor: 0 turns it off. Requires accounts:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
currency required | string | |
low_balance_minor required | integer |
Responses
- 200 The setting.
- 400
invalid_request.
Request
curl -X PUT "$KULPAY_URL/v1/admin/accounts/{id}/bill-balance-alert" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "MZN",
"low_balance_minor": 50000
}'
200 Response application/json
{
"channel_id": "3KWPkvN7G0ryWRJbNcBl0bbDmWS",
"currency": "MZN",
"low_balance_minor": 50000,
"updated_at": "2026-10-10T22:14:58.039503731Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no such account",
"error": {
"code": "invalid_request",
"message": "no such account"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/admin/bill-commissions
A channel's commission statement
As the channel sees it, for channel and month. Requires reconciliation:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
channel required | query | string | |
month | query | string |
Responses
- 200 The statement.
Request
curl -X GET "$KULPAY_URL/v1/admin/bill-commissions" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"channel_id": "3K4SjgBC4GZKqZAEojB1QDdbYiw",
"commission": "22.50",
"currency": "MZN",
"lines": [
{
"amount": "1000.00",
"bill_payment_id": "3K7sTNTzRrMVuuCSlaWe6IT5tcr",
"biller": "edm",
"commission": "15.00",
"completed_at": "2026-09-12T08:31:05Z",
"product": "credelec"
},
{
"amount": "500.00",
"bill_payment_id": "3K2xMacVPsuM57QuDBSLsOiRYpc",
"biller": "aguas_maputo",
"commission": "7.50",
"completed_at": "2026-09-21T18:02:47Z",
"product": "water"
}
],
"month": "2026-09",
"sales": 2,
"volume": "1500.00"
}
GET /v1/admin/bill-payments
Bill payments across channels
Every channel's bill payments, newest first, filtered by status (review is the review queue), biller, channel and customer_reference, with the request id, attempts and queries. Tokens and PINs are masked. Requires admin:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | |
biller | query | string | |
channel | query | string | |
customer_reference | query | string | |
limit | query | integer |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/bill-payments" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"amount": "250.00",
"artifacts": [
{
"type": "receipt",
"value": "FK2026101045631213"
},
{
"meta": {
"krn": "1",
"sgc": "600123",
"ti": "1"
},
"type": "token",
"unit_type": "kWh",
"units": "25.00",
"value": "****************7957"
}
],
"attempts": 1,
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.00",
"units": "25.00"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"channel_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"completed_at": "2026-10-10T22:14:58.744497415Z",
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"funding": "balance",
"id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"partner_reference": "POS-9",
"payer_fee": "5.00",
"product": "CREDELEC",
"product_version": 1,
"queries": 0,
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"sent_at": "2026-10-10T22:14:58.727064442Z",
"shape": "metered_prepaid",
"status": "succeeded",
"supplier_reference": "FK2026101045631213",
"total": "255.00"
}
],
"total": 1
}
GET /v1/admin/bill-payments/{id}
A bill payment (operators)
Tokens and PINs masked. Requires admin:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The bill payment.
- 404 Unknown.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/bill-payments/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"amount": "250.00",
"artifacts": [
{
"type": "receipt",
"value": "FK2026101045631213"
},
{
"meta": {
"krn": "1",
"sgc": "600123",
"ti": "1"
},
"type": "token",
"unit_type": "kWh",
"units": "25.00",
"value": "****************7957"
}
],
"attempts": 1,
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.00",
"units": "25.00"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"channel_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"completed_at": "2026-10-10T22:14:58.744497415Z",
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"funding": "balance",
"id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"partner_reference": "POS-9",
"payer_fee": "5.00",
"product": "CREDELEC",
"product_version": 1,
"queries": 0,
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"sent_at": "2026-10-10T22:14:58.727064442Z",
"shape": "metered_prepaid",
"status": "succeeded",
"supplier_reference": "FK2026101045631213",
"total": "255.00"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/bill-payments/{id}/artifacts
See tokens and PINs in clear (operators)
Operators otherwise see tokens and PINs masked. A reason is required and every use records bill_payment.artifacts_revealed with the operator. Requires bills:artifacts. An Idempotency-Key on this request binds the key to it as usual, but the success response holds a secret and is never kept (IG-30): the same key and request again is 409 idempotency_replay_unavailable (never the secret a second time; list the resources to find the one it made), another request under the key 409 idempotency_key_reused.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
reason required | string |
Responses
- 200
{artifacts}. - 400 No reason.
invalid_request - 404 Unknown.
not_found - 409
idempotency_replay_unavailable: a replay of a request that succeeded;idempotency_key_reused: another request under the key.
Request
curl -X POST "$KULPAY_URL/v1/admin/bill-payments/{id}/artifacts" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "support ticket 1"
}'
200 Response application/json
{
"artifacts": [
{
"type": "receipt",
"value": "FK2026101045631213"
},
{
"meta": {
"krn": "1",
"sgc": "600123",
"ti": "1"
},
"type": "token",
"unit_type": "kWh",
"units": "25.00",
"value": "69556183477675547957"
}
]
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "a reason is required",
"error": {
"code": "invalid_request",
"message": "a reason is required"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · idempotency_replay_unavailable application/problem+json
{
"code": "idempotency_replay_unavailable",
"detail": "A request with this Idempotency-Key already succeeded. Its response held a secret that is shown once and not kept, so it cannot be replayed: list the resources to find the one it created, and revoke it if its secret was lost.",
"error": {
"code": "idempotency_replay_unavailable",
"message": "A request with this Idempotency-Key already succeeded. Its response held a secret that is shown once and not kept, so it cannot be replayed: list the resources to find the one it created, and revoke it if its secret was lost."
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
POST /v1/admin/bill-payments/{id}/resolve
Resolve a bill payment in review
A payment goes to review when querying its supplier with the same request id has not settled it within 24 hours (BIL-2.5). An operator records what the supplier did: succeeded with the supplier's reference and, if one was issued, the token or PIN (sealed in the approval), or failed. A second operator approves; the payment then succeeds (the reserve settled, bill_payment.succeeded) or fails (released, bill_payment.failed), with bill_payment.resolved. Requires admin:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
outcome required | string | One of: succeeded, failed |
reason required | string | |
supplier_reference | string | |
token | string | |
units | string |
Responses
- 202
{approval}. - 400
invalid_request: not in review, or a delivery without the supplier's reference. - 404 Unknown.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/bill-payments/{id}/resolve" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"outcome": "succeeded",
"reason": "Pagamento de energia, outubro",
"supplier_reference": "2003014567",
"token": "tok_7Hq2…",
"units": "string"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-11T10:02:37Z",
"id": "3KUYO4KZt1AMD7NPEGgTx2VrIB7",
"kind": "bill.resolve",
"payload": "{\"outcome\":\"failed\"}",
"prepared_by": "8f3c2a7e5b1d4c6a9e0f2b7d4a1c5e83",
"reason": "EDM confirmed by phone that no token was issued for this request",
"status": "pending",
"subject_id": "3KUYJ7FUo4vHy1IKzBbOs5QmDw2",
"tenant_id": "3KUXV2rGa7hTj4uWkMnAe8CyPi8"
}
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "only a payment in review is resolved by hand (this one is succeeded)",
"error": {
"code": "invalid_request",
"message": "only a payment in review is resolved by hand (this one is succeeded)"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/bill-recon
Supplier reconciliation runs
Recent runs with their breaks, of a connector or all. Requires reconciliation:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
connector | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/bill-recon" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"breaks": 1,
"breaks_list": [
{
"customer_reference": "07011122233",
"kind": "supplier_only",
"request_id": "3KUZD4ZOI1PBS7CETVvIM2KGgQ2",
"supplier_amount_minor": 50000,
"supplier_reference": "EDM-20261010-559102"
}
],
"connector": "edm_live",
"connector_id": "3KUXT4pDy6eRf8sUhJkYc2AwMg6",
"created_at": "2026-10-11T06:00:41Z",
"day": "2026-10-10T00:00:00Z",
"id": "3KUYI4ETn1uGx7HJyAaNr2PlCv1",
"kulpay_count": 1287,
"matched": 1286,
"source": "pull",
"supplier_count": 1287
}
],
"total": 1
}
GET /v1/admin/billers
Billers (operators)
Every biller, any status, filterable by status, category and q. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | One of: draft, active, suspended, retired |
category | query | string | |
q | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/billers" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"allow_overpay": false,
"allow_partial": false,
"category": "electricity",
"code": "EDM",
"country": "MZ",
"created_at": "2026-03-02T08:15:00Z",
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"logo_url": "https://cdn.example.co.mz/billers/edm.png",
"name": "Electricidade de Moçambique",
"presentment_modes": [],
"status": "active",
"support_email": "apoioexample.co.mz",
"support_phone": "+258841234567",
"updated_at": "2026-09-18T10:42:11Z"
},
{
"allow_overpay": false,
"allow_partial": true,
"category": "water",
"code": "AGUAS_MAPUTO",
"country": "MZ",
"created_at": "2026-04-11T09:00:00Z",
"id": "3KUXR1mVq8sLd2tPfGhZa7YwKe4",
"invoice_expiry_days": 30,
"name": "Águas da Região de Maputo",
"presentment_modes": [
"push",
"lookup"
],
"remit_account": "000800001234567890157",
"remit_bank": "BCI",
"remit_frequency": "daily",
"status": "active",
"support_email": "clientesexample.co.mz",
"tenant_id": "3KUXS7nBw2cQe5rTgHjXb9ZvLf5",
"updated_at": "2026-10-01T14:20:37Z"
}
],
"total": 2
}
POST /v1/admin/billers
Register a biller
Creates a biller as a draft (BIL-1.1); channels see it once a second operator activates it. Records biller.created. Requires connectors:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
category | string | One of: electricity, water, telecom, tv, internet, education, insurance, government, other |
code | string | |
country | string | |
logo_url | string (uri) | |
name | string | |
support_email | string (email) | |
support_phone | string | |
tenant_id | string | An onboarded organisation with the biller role, if any. |
Responses
- 201 The biller.
- 400
invalid_request.
Request
curl -X POST "$KULPAY_URL/v1/admin/billers" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"category": "water",
"code": "AGUAS",
"country": "MZ",
"invoice_expiry_days": 30,
"name": "Águas",
"presentment_modes": [
"push"
],
"tenant_id": "3KWPl3PNWBvbCepPhJBUlOXiLUx"
}'
201 Response application/json
{
"allow_overpay": false,
"allow_partial": false,
"category": "water",
"code": "AGUAS",
"country": "MZ",
"created_at": "2026-10-10T22:14:59.252334782Z",
"id": "3KWPlFlovXTX5hJHsBDDV47dyHf",
"invoice_expiry_days": 30,
"name": "Águas",
"presentment_modes": [
"push"
],
"status": "draft",
"tenant_id": "3KWPl3PNWBvbCepPhJBUlOXiLUx",
"updated_at": "2026-10-10T22:14:59.252334782Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "category is one of electricity, water, telecom, tv, internet, education, insurance, government, other",
"error": {
"code": "invalid_request",
"message": "category is one of electricity, water, telecom, tv, internet, education, insurance, government, other"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/admin/billers/{code}
A biller with its products
Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
Responses
- 200
{biller, products}. - 404 No such biller, product or supply connector.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"biller": {
"allow_overpay": false,
"allow_partial": false,
"category": "tv",
"code": "TVCABO",
"country": "MZ",
"created_at": "2026-10-10T22:14:57.786577491Z",
"id": "3KWPkylzKmRsF6Rq3xef8pjun2x",
"name": "TV Cabo",
"presentment_modes": [],
"status": "active",
"support_phone": "+258 21 480 500",
"updated_at": "2026-10-10T22:14:57.920983259Z"
},
"products": [
{
"allowed_channels": null,
"amount_rule": "fixed",
"biller_id": "3KWPkylzKmRsF6Rq3xef8pjun2x",
"code": "PACOTE",
"connectors": [
"3KWPkweeFBr3p9OxYa5dTFjzMCF"
],
"created_at": "2026-10-10T22:14:57.814553026Z",
"currency": "MZN",
"denominations": null,
"enabled": true,
"fee_bearer": "payer",
"fee_bps": 0,
"fee_fixed_minor": 500,
"fixed_minor": 75000,
"id": "3KWPkv1wLq7YHJt6Jqg48mMzOKS",
"lookup_required": false,
"model": "agent",
"name": "Pacote",
"reference_label": "Smartcard",
"reference_pattern": "^\\d{10}$",
"shape": "account_topup",
"supplier_product": "tv",
"updated_at": "2026-10-10T22:14:57.814553026Z",
"version": 1
}
]
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PATCH /v1/admin/billers/{code}
Change a biller's details
Fields left out keep their value; the status changes only through /status. Records biller.updated. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
category | string | One of: electricity, water, telecom, tv, internet, education, insurance, government, other |
code | string | |
country | string | |
logo_url | string (uri) | |
name | string | |
support_email | string (email) | |
support_phone | string | |
tenant_id | string | An onboarded organisation with the biller role, if any. |
Responses
- 200 The biller.
- 400
invalid_request. - 404 No such biller, product or supply connector.
not_found
Request
curl -X PATCH "$KULPAY_URL/v1/admin/billers/{code}" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"presentment_modes": [
"push",
"file"
]
}'
200 Response application/json
{
"allow_overpay": false,
"allow_partial": false,
"category": "water",
"code": "AGUAS",
"country": "MZ",
"created_at": "2026-10-10T22:14:59.252334782Z",
"id": "3KWPlFlovXTX5hJHsBDDV47dyHf",
"invoice_expiry_days": 30,
"name": "Águas",
"presentment_modes": [
"push",
"file"
],
"status": "active",
"tenant_id": "3KWPl3PNWBvbCepPhJBUlOXiLUx",
"updated_at": "2026-10-10T22:14:59.846639591Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "support_email is not an email address",
"error": {
"code": "invalid_request",
"message": "support_email is not an email address"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/accounts/{reference}/qr
A meter's or account's QR (operators)
A QR for a meter or customer account of a biller with no portal (BIL-5.13): a Credelec meter, an airtime number. Its page buys for the meter through the hosted checkout (amount and the payer's phone, which gets the token by SMS). As GET /v1/biller/accounts/{reference}/qr. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
reference required | path | string | The customer account or meter number: 1 to 40 printable characters, no spaces. |
product | query | string | The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it. |
Responses
- 200 The account's QR.
- 400
invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active. - 404 No such biller.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/accounts/{reference}/qr" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"biller": "AGUAS",
"mode": "live",
"png_url": "/v1/biller/accounts/CL-001234/qr.png",
"reference": "CL-0042",
"svg_url": "/v1/biller/accounts/CL-001234/qr.svg",
"url": "https://pay.kulpay.co.mz/pay/account/bB9BR1VBUx8fQ0wtMDA0Mg.3oT2Lw9sQe1Zk7mN"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no product of AGUAS takes CL-****2 as a reference",
"error": {
"code": "invalid_request",
"message": "no product of AGUAS takes CL-****2 as a reference"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/accounts/{reference}/qr.png
A meter's or account's QR, PNG (operators)
A QR for a meter or customer account of a biller with no portal (BIL-5.13): a Credelec meter, an airtime number. Its page buys for the meter through the hosted checkout (amount and the payer's phone, which gets the token by SMS). As GET /v1/biller/accounts/{reference}/qr. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
reference required | path | string | The customer account or meter number: 1 to 40 printable characters, no spaces. |
product | query | string | The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 400
invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active. - 404 No such biller.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/accounts/{reference}/qr.png" \
-H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no product of AGUAS takes CL-****2 as a reference",
"error": {
"code": "invalid_request",
"message": "no product of AGUAS takes CL-****2 as a reference"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/accounts/{reference}/qr.svg
A meter's or account's QR, rendered (operators)
A QR for a meter or customer account of a biller with no portal (BIL-5.13): a Credelec meter, an airtime number. Its page buys for the meter through the hosted checkout (amount and the payer's phone, which gets the token by SMS). As GET /v1/biller/accounts/{reference}/qr. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
reference required | path | string | The customer account or meter number: 1 to 40 printable characters, no spaces. |
product | query | string | The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 400
invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active. - 404 No such biller.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/accounts/{reference}/qr.svg" \
-H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no product of AGUAS takes CL-****2 as a reference",
"error": {
"code": "invalid_request",
"message": "no product of AGUAS takes CL-****2 as a reference"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/products
Configure a product
A product (BIL-1.2): its shape, amount rule, customer reference, supply connectors in priority order, commercial model and fee. The fee is borne by the payer or by the biller, never both (422 fee_bearer_conflict): a payer fee cannot come with a supplier discount, the agent model has no discount, and on the principal model with the biller bearing, the discount is the fee. Records bill_product.created. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount_rule | string | One of: range, fixed, denominations, presented |
code | string | |
connectors | string[] | Supply connector names, in priority order. |
currency | string | |
denominations | integer[] | |
discount_bps | integer | The supplier's discount to KulPay in basis points (principal model, biller bears the fee). |
enabled | boolean | |
fee_bearer | string | One of: payer, biller |
fee_bps | integer | Percentage fee in basis points (150 = 1.50%). |
fee_fixed_minor | integer | Fixed fee. |
fee_max_minor | integer | Maximum fee. |
fee_min_minor | integer | Minimum fee. |
fixed_minor | integer | Fixed amount. |
lookup_required | boolean | |
max_minor | integer | Range maximum. |
min_minor | integer | Range minimum. |
model | string | One of: principal, agent |
name | string | |
purchase_modes | string[] | How the product may be bought: `fixed_catalog` (an offer from its catalogue) and/or `variable_amount` (an amount under `amount_rule`). Both by default; a catalogue-only product needs no `amount_rule`. |
reference_example | string | |
reference_label | string | |
reference_pattern | string | A regular expression the reference must match. |
routable | boolean | A lookup that names no biller is sent to this biller when the reference matches reference_pattern (BIL-5.14); needs reference_pattern. |
shape | string | One of: metered_prepaid, account_topup, airtime, postpaid, voucher |
supplier_product | string | The product's code at the supplier. |
Responses
- 201 The product, version 1.
- 400
invalid_request. - 404 No such biller, product or supply connector.
not_found - 422
fee_bearer_conflict.
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_rule": "range",
"code": "CREDELEC",
"connectors": [
"fake-test"
],
"currency": "MZN",
"fee_bearer": "payer",
"fee_fixed_minor": 500,
"lookup_required": true,
"max_minor": 5000000,
"min_minor": 2000,
"model": "principal",
"name": "Credelec",
"reference_label": "Contador",
"reference_pattern": "^\\d{11}$",
"shape": "metered_prepaid"
}'
201 Response application/json
{
"allowed_channels": null,
"amount_rule": "fixed",
"biller_id": "3KWPkylzKmRsF6Rq3xef8pjun2x",
"code": "PACOTE",
"connectors": [
"3KWPkweeFBr3p9OxYa5dTFjzMCF"
],
"created_at": "2026-10-10T22:14:57.814553026Z",
"currency": "MZN",
"denominations": null,
"enabled": true,
"fee_bearer": "payer",
"fee_bps": 0,
"fee_fixed_minor": 500,
"fixed_minor": 75000,
"id": "3KWPkv1wLq7YHJt6Jqg48mMzOKS",
"lookup_required": false,
"model": "agent",
"name": "Pacote",
"reference_label": "Smartcard",
"reference_pattern": "^\\d{10}$",
"shape": "account_topup",
"supplier_product": "tv",
"updated_at": "2026-10-10T22:14:57.814553026Z",
"version": 1
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "reference_label is required: what the payer enters (meter number, phone number…)",
"error": {
"code": "invalid_request",
"message": "reference_label is required: what the payer enters (meter number, phone number…)"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · fee_bearer_conflict application/problem+json
{
"code": "fee_bearer_conflict",
"detail": "the payer bears the fee, so the biller cannot also give a discount: one party pays, never both",
"error": {
"code": "fee_bearer_conflict",
"message": "the payer bears the fee, so the biller cannot also give a discount: one party pays, never both"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
PATCH /v1/admin/billers/{code}/products/{product}
Change a product
Fields left out keep their value. Every change is a new version; payments keep the version they were quoted under. Records bill_product.updated. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
product required | path | string | The product code, e.g. `CREDELEC`. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount_rule | string | One of: range, fixed, denominations, presented |
code | string | |
connectors | string[] | Supply connector names, in priority order. |
currency | string | |
denominations | integer[] | |
discount_bps | integer | The supplier's discount to KulPay in basis points (principal model, biller bears the fee). |
enabled | boolean | |
fee_bearer | string | One of: payer, biller |
fee_bps | integer | Percentage fee in basis points (150 = 1.50%). |
fee_fixed_minor | integer | Fixed fee. |
fee_max_minor | integer | Maximum fee. |
fee_min_minor | integer | Minimum fee. |
fixed_minor | integer | Fixed amount. |
lookup_required | boolean | |
max_minor | integer | Range maximum. |
min_minor | integer | Range minimum. |
model | string | One of: principal, agent |
name | string | |
purchase_modes | string[] | How the product may be bought: `fixed_catalog` (an offer from its catalogue) and/or `variable_amount` (an amount under `amount_rule`). Both by default; a catalogue-only product needs no `amount_rule`. |
reference_example | string | |
reference_label | string | |
reference_pattern | string | A regular expression the reference must match. |
routable | boolean | A lookup that names no biller is sent to this biller when the reference matches reference_pattern (BIL-5.14); needs reference_pattern. |
shape | string | One of: metered_prepaid, account_topup, airtime, postpaid, voucher |
supplier_product | string | The product's code at the supplier. |
Responses
- 200 The product.
- 400
invalid_request. - 404 No such biller, product or supply connector.
not_found - 422
fee_bearer_conflict.
Request
curl -X PATCH "$KULPAY_URL/v1/admin/billers/{code}/products/{product}" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_rule": "range",
"code": "EDM",
"connectors": [
"string"
],
"currency": "MZN",
"denominations": [
1
],
"discount_bps": 12,
"enabled": true,
"fee_bearer": "payer",
"fee_bps": 1,
"fee_fixed_minor": 1,
"fee_max_minor": 1,
"fee_min_minor": 1,
"fixed_minor": 1,
"lookup_required": true,
"max_minor": 1,
"min_minor": 1,
"model": "principal",
"name": "Electricidade de Moçambique",
"reference_example": "2003014567",
"reference_label": "2003014567",
"reference_pattern": "2003014567",
"shape": "metered_prepaid",
"supplier_product": "string"
}'
200 Response application/json
{
"allowed_channels": null,
"amount_rule": "range",
"biller_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"code": "CREDELEC",
"connectors": [
"3KUXT4pDy6eRf8sUhJkYc2AwMg6"
],
"created_at": "2026-03-02T08:30:00Z",
"currency": "MZN",
"denominations": null,
"enabled": true,
"fee_bearer": "payer",
"fee_bps": 150,
"fee_fixed_minor": 0,
"fee_max_minor": 10000,
"fee_min_minor": 500,
"id": "3KUXU9qFz3gSh1tViLmZd5BxNh7",
"limit_channel_daily_minor": 50000000,
"limit_reference_daily_count": 5,
"lookup_required": true,
"max_minor": 5000000,
"min_minor": 5000,
"model": "principal",
"name": "Credelec pré-pago",
"reference_example": "07012345678",
"reference_label": "Número do contador",
"reference_pattern": "^[0-9]{11}$",
"shape": "metered_prepaid",
"supplier_product": "CREDELEC",
"updated_at": "2026-10-11T09:12:44Z",
"version": 4
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "fees are non-negative, percentages at most 100%, and fee_min_minor <= fee_max_minor",
"error": {
"code": "invalid_request",
"message": "fees are non-negative, percentages at most 100%, and fee_min_minor <= fee_max_minor"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · fee_bearer_conflict application/problem+json
{
"code": "fee_bearer_conflict",
"detail": "the payer bears the fee, so the biller cannot also give a discount: one party pays, never both",
"error": {
"code": "fee_bearer_conflict",
"message": "the payer bears the fee, so the biller cannot also give a discount: one party pays, never both"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
PUT /v1/admin/billers/{code}/products/{product}/channels
Restrict a product to channels
The channel accounts that may sell the product; an empty list opens it to every channel (BIL-1.3). Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
product required | path | string | The product code, e.g. `CREDELEC`. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
channels | string[] |
Responses
- 200 The product.
- 400
invalid_request. - 404 No such biller, product or supply connector.
not_found
Request
curl -X PUT "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/channels" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"channels": [
"string"
]
}'
200 Response application/json
{
"allowed_channels": [
"3KUXV2rGa7hTj4uWkMnAe8CyPi8",
"3KUXW5sHb1iUk7vXmNoBf3DzQj9"
],
"amount_rule": "range",
"biller_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"code": "CREDELEC",
"connectors": [
"3KUXT4pDy6eRf8sUhJkYc2AwMg6"
],
"created_at": "2026-03-02T08:30:00Z",
"currency": "MZN",
"denominations": null,
"enabled": true,
"fee_bearer": "payer",
"fee_bps": 150,
"fee_fixed_minor": 0,
"fee_max_minor": 10000,
"fee_min_minor": 500,
"id": "3KUXU9qFz3gSh1tViLmZd5BxNh7",
"lookup_required": true,
"max_minor": 5000000,
"min_minor": 5000,
"model": "principal",
"name": "Credelec pré-pago",
"reference_example": "07012345678",
"reference_label": "Número do contador",
"reference_pattern": "^[0-9]{11}$",
"shape": "metered_prepaid",
"supplier_product": "CREDELEC",
"updated_at": "2026-10-11T09:20:03Z",
"version": 5
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no channel account 3KUXX8tJc4jVm1wYnPpCg6EaRk0",
"error": {
"code": "invalid_request",
"message": "no channel account 3KUXX8tJc4jVm1wYnPpCg6EaRk0"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/products/{product}/integration
A product's notification and report (admin)
The product's own integration with its LSP in a mode (BIL-5.16), secrets masked; defaults when none was set. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
Responses
- 200 The integration.
- 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"biller": "AGUAS",
"fields": [
"event",
"notification_id",
"payment_id",
"request_id",
"mode",
"biller",
"biller_name",
"product",
"supplier_product",
"reference",
"invoice_number",
"amount",
"payer_fee",
"total",
"currency",
"amount_minor",
"total_minor",
"paid_at",
"channel_id",
"channel_name",
"funding",
"partner_reference",
"supplier_reference",
"receipt_url"
],
"mode": "test",
"notification": {
"auth": "hmac",
"content_type": "application/xml",
"enabled": true,
"headers": {
"X-Biller-Code": "AGUAS-01"
},
"method": "POST",
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
"url": "https://lsp.example/kulpay/pagamentos"
},
"product": "FACTURA",
"report": {
"auth": "bearer",
"columns": [
{
"field": "paid_at",
"header": "DATA",
"width": 10
},
{
"field": "invoice_number",
"header": "FACTURA",
"width": 12
},
{
"align": "right",
"field": "total",
"header": "VALOR",
"width": 10
}
],
"date_format": "DD/MM/YYYY",
"decimal_separator": ",",
"enabled": true,
"file_name": "AGUAS_{date}.dat",
"format": "txt",
"header": true,
"last_day": "2026-10-19",
"line_ending": "lf",
"url": "https://lsp.example/kulpay/reconciliacao"
},
"updated_at": "2026-10-20T08:00:00Z",
"version": 3
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PUT /v1/admin/billers/{code}/products/{product}/integration
Set a product's notification and report (admin)
Sets the payment notification (URL, body template, content type, auth, headers) and the daily report (format, columns, file name, URL) of a product in a mode (BIL-5.16). Fields left out keep their value. Each change is a new version (bill_product.integration_set). The change takes effect at once, in live too, with no second approval (owner decision 2026-10-20). Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
Request body application/json required
| Field | Type | Description |
|---|---|---|
notification | object | The payment notification: every payment of the product that succeeds is sent to `url`, its body rendered from `template` (Go text/template over the fields; helpers `json`, `xml`, `url` escape a value, `date "DD/MM/YYYY HH:mm"` formats paid_at, `cents` drops the decimal point). Checked when set: the body must be what `content_type` says. Retried for 24 h; kept for resends. |
notification.auth | object | How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only. |
notification.auth.password | string | |
notification.auth.secret | string | |
notification.auth.token | string | |
notification.auth.type | string | One of: none, bearer, basic, hmac |
notification.auth.username | string | |
notification.content_type | string | One of: application/json, application/xml, application/x-www-form-urlencoded, text/plain |
notification.enabled | boolean | |
notification.headers | object | Up to 10 static headers; not Authorization, Content-Type or KulPay-*. |
notification.method | string | One of: POST, PUT |
notification.template | string | |
notification.url | string (uri) | |
report | object | The daily report: the product's payments of a day, in the LSP's format and file extension; downloadable, and sent to `url` the next day. |
report.auth | object | How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only. |
report.auth.password | string | |
report.auth.secret | string | |
report.auth.token | string | |
report.auth.type | string | One of: none, bearer, basic, hmac |
report.auth.username | string | |
report.columns | object[] | |
report.columns[].align | string | One of: left, right |
report.columns[].field required | string | One of: event, notification_id, payment_id, request_id, mode, biller, biller_name, product, supplier_product, reference, invoice_number, amount, payer_fee, total, currency, amount_minor, total_minor, paid_at, channel_id, channel_name, funding, partner_reference, supplier_reference, receipt_url |
report.columns[].header | string | |
report.columns[].width | integer | txt (fixed width) only. |
report.date_format | string | |
report.decimal_separator | string | One of: ., , |
report.enabled | boolean | |
report.file_name | string | Placeholders {biller}, {product}, {date}, {mode}; the extension is the one given, else the format's. |
report.format | string | One of: csv, tsv, psv, txt, json, xml, xlsx |
report.header | boolean | |
report.line_ending | string | One of: lf, crlf |
report.url | string (uri) |
Responses
- 200 The integration.
- 400
invalid_request. - 404 No such biller or product.
not_found
Request
curl -X PUT "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"notification": {
"auth": {
"secret": "<o seu segredo>",
"type": "hmac"
},
"content_type": "application/xml",
"enabled": true,
"headers": {
"X-Biller-Code": "AGUAS-01"
},
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
"url": "https://lsp.example/kulpay/pagamentos"
},
"report": {
"auth": {
"token": "<o seu token>",
"type": "bearer"
},
"columns": [
{
"field": "paid_at",
"header": "DATA",
"width": 10
},
{
"field": "invoice_number",
"header": "FACTURA",
"width": 12
},
{
"align": "right",
"field": "total",
"header": "VALOR",
"width": 10
}
],
"date_format": "DD/MM/YYYY",
"decimal_separator": ",",
"enabled": true,
"file_name": "AGUAS_{date}.dat",
"format": "txt",
"url": "https://lsp.example/kulpay/reconciliacao"
}
}'
200 Response application/json
{
"biller": "AGUAS",
"fields": [
"event",
"notification_id",
"payment_id",
"request_id",
"mode",
"biller",
"biller_name",
"product",
"supplier_product",
"reference",
"invoice_number",
"amount",
"payer_fee",
"total",
"currency",
"amount_minor",
"total_minor",
"paid_at",
"channel_id",
"channel_name",
"funding",
"partner_reference",
"supplier_reference",
"receipt_url"
],
"mode": "test",
"notification": {
"auth": "hmac",
"content_type": "application/xml",
"enabled": true,
"headers": {
"X-Biller-Code": "AGUAS-01"
},
"method": "POST",
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
"url": "https://lsp.example/kulpay/pagamentos"
},
"product": "FACTURA",
"report": {
"auth": "bearer",
"columns": [
{
"field": "paid_at",
"header": "DATA",
"width": 10
},
{
"field": "invoice_number",
"header": "FACTURA",
"width": 12
},
{
"align": "right",
"field": "total",
"header": "VALOR",
"width": 10
}
],
"date_format": "DD/MM/YYYY",
"decimal_separator": ",",
"enabled": true,
"file_name": "AGUAS_{date}.dat",
"format": "txt",
"header": true,
"last_day": "2026-10-19",
"line_ending": "lf",
"url": "https://lsp.example/kulpay/reconciliacao"
},
"updated_at": "2026-10-20T08:00:00Z",
"version": 3
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "notification: the template does not render well-formed XML (use {{xml .field}} for values)",
"error": {
"code": "invalid_request",
"message": "notification: the template does not render well-formed XML (use {{xml .field}} for values)"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/products/{product}/integration/deliveries
A product's notifications and reports sent (admin)
The deliveries to the LSP, newest first: status pending (retrying), delivered or exhausted, attempts, the endpoint's last answer. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
kind | query | string | One of: notification, report |
Responses
- 200 The deliveries.
- 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/deliveries" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"attempts": 1,
"content_type": "application/xml",
"created_at": "2026-10-20T08:00:00Z",
"delivered_at": "2026-10-20T08:00:01Z",
"duration_ms": 84,
"id": "3KvxDelivery00000000000001",
"integration_id": "3KvxIntegration0000000001",
"kind": "notification",
"method": "POST",
"mode": "test",
"next_attempt_at": "2026-10-20T08:00:00Z",
"response_code": 200,
"status": "delivered",
"subject_id": "3KvxPayment000000000000001",
"updated_at": "2026-10-20T08:00:01Z",
"url": "https://lsp.example/kulpay/pagamentos"
}
]
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}
One delivery and its exact body (admin)
What was sent, byte for byte: body (text) or body_base64 (xlsx). Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
id required | path | string | The delivery id. |
Responses
- 200 The delivery.
- 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"body": "<pagamento><referencia>CL-1</referencia><valor>151000</valor><data>19/10/2026 12:00</data></pagamento>",
"delivery": {
"attempts": 1,
"content_type": "application/xml",
"created_at": "2026-10-20T08:00:00Z",
"delivered_at": "2026-10-20T08:00:01Z",
"duration_ms": 84,
"id": "3KvxDelivery00000000000001",
"integration_id": "3KvxIntegration0000000001",
"kind": "notification",
"method": "POST",
"mode": "test",
"next_attempt_at": "2026-10-20T08:00:00Z",
"response_code": 200,
"status": "delivered",
"subject_id": "3KvxPayment000000000000001",
"updated_at": "2026-10-20T08:00:01Z",
"url": "https://lsp.example/kulpay/pagamentos"
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}/retry
Send a delivery again (admin)
Sends a pending or exhausted delivery again now, the same body. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
id required | path | string | The delivery id. |
Responses
- 200 The delivery after the attempt.
- 404 No such biller or product.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}/retry" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"body": "<pagamento>…</pagamento>",
"delivery": {
"attempts": 1,
"content_type": "application/xml",
"created_at": "2026-10-20T08:00:00Z",
"delivered_at": "2026-10-20T08:00:01Z",
"duration_ms": 84,
"id": "3KvxDelivery00000000000001",
"integration_id": "3KvxIntegration0000000001",
"kind": "notification",
"method": "POST",
"mode": "test",
"next_attempt_at": "2026-10-20T08:00:00Z",
"response_code": 200,
"status": "delivered",
"subject_id": "3KvxPayment000000000000001",
"updated_at": "2026-10-20T08:00:01Z",
"url": "https://lsp.example/kulpay/pagamentos"
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/products/{product}/integration/preview
Preview a notification's body (admin)
Renders a template (the one given, else the product's) for a sample payment, as the LSP would receive it. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
Request body application/json required
| Field | Type | Description |
|---|---|---|
content_type | string | |
template | string |
Responses
- 200 The rendered body.
- 400
invalid_request. - 404 No such biller or product.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/preview" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"content_type": "application/xml",
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>"
}'
200 Response application/json
{
"body": "<pagamento><referencia>EX-123456</referencia><valor>126000</valor><data>19/10/2026 16:30</data></pagamento>",
"content_type": "application/xml"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "template: unexpected \"}\" in operand",
"error": {
"code": "invalid_request",
"message": "template: unexpected \"}\" in operand"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/products/{product}/limits
Change a product's daily limits
Per day (Maputo time): payments to one customer reference (reference_daily_count, reference_daily_minor) and the amount one channel sells (channel_daily_minor); 0 removes a limit (BIL-6.4). A second operator approves. Quotes and payments beyond a limit are refused with 422 limit_exceeded. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | |
product required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
channel_daily_minor | integer | |
reason required | string | |
reference_daily_count | integer | |
reference_daily_minor | integer |
Responses
- 202
{approval}. - 404 Unknown product.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/limits" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel_daily_minor": 1000000,
"reason": "policy",
"reference_daily_count": 3
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-10T22:15:00.011523819Z",
"id": "3KWPlO70sHMc9BFjsbRiot7VUEk",
"kind": "bill.limits",
"payload": "{\"reference_daily_count\":3,\"reference_daily_minor\":0,\"channel_daily_minor\":1000000}",
"prepared_by": "key_accounts",
"reason": "policy",
"status": "pending",
"subject_id": "3KWPlDIPesBSi4VERFPanmdB27f",
"tenant_id": "platform"
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/products/{product}/offers
Create or change an offer
A bundle or bouquet of the product, by code. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
product required | path | string | The product code, e.g. `CREDELEC`. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
active | boolean | |
code required | string | |
name required | string | |
price_minor required | integer | Price. |
validity | string |
Responses
- 200 The offer.
- 400
invalid_request. - 404 No such biller, product or supply connector.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/offers" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"active": true,
"code": "EDM",
"name": "Electricidade de Moçambique",
"price_minor": 1,
"validity": "30d"
}'
200 Response application/json
{
"active": true,
"code": "TVBASIC",
"id": "3KUXY1uKd7kWn4xZoQqDh9FbSl1",
"name": "Pacote Básico",
"price_minor": 75000,
"updated_at": "2026-10-11T09:25:18Z",
"validity": "30d"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "an offer needs a code (capital letters, digits, _), a name and a positive price_minor",
"error": {
"code": "invalid_request",
"message": "an offer needs a code (capital letters, digits, _), a name and a positive price_minor"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/products/{product}/offers/refresh
Refresh offers from the supplier
Replaces the product's offers with those its first supply connector lists, when the driver lists offers. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
product required | path | string | The product code, e.g. `CREDELEC`. |
Responses
- 200
{refreshed}. - 400
invalid_request. - 404 No such biller, product or supply connector.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/offers/refresh" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"refreshed": 2
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the edm driver does not list offers",
"error": {
"code": "invalid_request",
"message": "the edm driver does not list offers"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/billers/{code}/products/{product}/reports/{day}
A product's report for a day (admin)
The product's payments that succeeded on the day (Maputo), in its report format and file name (Content-Disposition). Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code. |
product required | path | string | The product code (KulPay's). |
mode | query | string | The mode whose integration (the LSP's test and live endpoints differ). Default: the key's. One of: live, test |
day required | path | string (date) | The day, YYYY-MM-DD, Maputo time. |
Responses
- 200 The file, in the product's format.
- 400
invalid_request. - 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/reports/{day}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response text/plain
DATA FACTURA VALOR 19/10/2026F-N1 1510,00
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the day is YYYY-MM-DD",
"error": {
"code": "invalid_request",
"message": "the day is YYYY-MM-DD"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/billers/{code}/remittances
Remit a biller now
Creates the biller's remittances of what KulPay collected for it on the agent model and has not remitted, less the fees it bears, one per mode and currency (BIL-5.7). Billers with a bank account and a remit_frequency are remitted daily or weekly by job. Records bill_remittance.created (a webhook to the biller). Requires reconciliation:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string |
Responses
- 200
{data, total}: the remittances created, none when nothing is owed.
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/remittances" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"count": 1,
"created_at": "2026-10-10T22:14:59.538329669Z",
"currency": "MZN",
"fee_minor": 0,
"gross_minor": 150000,
"id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
"mode": "test",
"net_minor": 150000,
"period_from": "2026-10-10T22:14:59.495269929Z",
"period_to": "2026-10-10T22:14:59.495269929Z",
"status": "pending"
}
],
"total": 1
}
POST /v1/admin/billers/{code}/status
Activate, suspend or retire a biller
Prepares the change for a second operator (maker-checker); approving it records biller.status_changed. Activation needs an enabled product with a supply connector. A suspended biller refuses new payments. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
reason required | string | |
status required | string | One of: active, suspended, retired |
Responses
- 202
{approval}. - 400
invalid_request. - 404 No such biller, product or supply connector.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/status" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "contract",
"status": "active"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-10T22:14:57.89256229Z",
"id": "3KWPkwtupluYYLGAw7At48TbMyG",
"kind": "bill.biller_status",
"payload": "{\"status\":\"active\"}",
"prepared_by": "key_accounts",
"reason": "contract",
"status": "pending",
"subject_id": "3KWPkylzKmRsF6Rq3xef8pjun2x",
"tenant_id": "platform"
}
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the biller needs an enabled product with a supply connector before it is activated",
"error": {
"code": "invalid_request",
"message": "the biller needs an enabled product with a supply connector before it is activated"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/commission-plans
Commission plans
Of a channel (and the plans for all channels) or all. Requires accounts:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
channel | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/commission-plans" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"approval_id": "3KWPlGFxMcmtqjHW45BKc6Jqgkq",
"bps": 150,
"channel_id": "3KWPl9fd5z3WDdnaGzpvnzDkLUW",
"created_at": "2026-10-10T22:14:59.95764134Z",
"created_by": "key_accounts",
"direction": "we_pay",
"effective_from": "2026-10-10T22:14:59.95764134Z",
"id": "3KWPlHvsSlANI9980QIV6EKj9y4",
"status": "active",
"version": 1
}
],
"total": 1
}
POST /v1/admin/commission-plans
Propose a commission plan
What a channel earns (we_pay) or pays (we_charge) per bill payment it sells (BIL-3.3): for channel_id or all channels, a biller's product or all, fixed_minor plus bps, optionally tiers [{from_minor, bps}] by the channel's volume this month, from effective_from. A second person activates it (the plan it replaces for the same scope ends). Payments accrue against the plan in force when quoted, when they succeed (bill_commission.accrued), credited to or debited from the channel's balance. Requires accounts:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
biller | string | |
bps | integer | |
channel_id | string | |
direction required | string | One of: we_pay, we_charge |
effective_from | string (date) | |
fixed_minor | integer | |
product | string | |
reason required | string | |
tiers | object[] | |
tiers[].bps | integer | |
tiers[].from_minor | integer |
Responses
- 202 The plan,
pending, with itsapproval_id. - 400
invalid_request.
Request
curl -X POST "$KULPAY_URL/v1/admin/commission-plans" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"bps": 150,
"channel_id": "3KWPl9fd5z3WDdnaGzpvnzDkLUW",
"direction": "we_pay",
"reason": "agents"
}'
202 Response application/json
{
"approval_id": "3KWPlGFxMcmtqjHW45BKc6Jqgkq",
"bps": 150,
"channel_id": "3KWPl9fd5z3WDdnaGzpvnzDkLUW",
"created_at": "2026-10-10T22:14:59.95764134Z",
"created_by": "key_accounts",
"direction": "we_pay",
"effective_from": "2026-10-10T22:14:59.95764134Z",
"id": "3KWPlHvsSlANI9980QIV6EKj9y4",
"status": "pending",
"version": 1
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "direction is we_pay or we_charge",
"error": {
"code": "invalid_request",
"message": "direction is we_pay or we_charge"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/admin/float-topups
Float top-ups
Newest first, of one connector or all. Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
connector | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/float-topups" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"amount_minor": 500000,
"approval_id": "3KWPlAON5BFKuGPEjNuGpBV06KB",
"bank_reference": "BCI-1",
"connector_id": "3KWPkweeFBr3p9OxYa5dTFjzMCF",
"created_at": "2026-10-10T22:14:58.017507082Z",
"created_by": "key_accounts",
"currency": "MZN",
"id": "3KWPl6P3YYG5B9DI0Klb9fMlSUu",
"status": "pending"
}
],
"total": 1
}
GET /v1/admin/reference-routes
Where a reference would be routed (operators)
Which billers a reference lookup that names no biller would ask (BIL-5.14): the active billers whose routable products' reference_pattern matches, for a channel when given. too_many when more than 5 match (the lookup answers biller_required). Requires connectors:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
reference required | query | string | |
channel | query | string | A channel account: only products open to it. |
Responses
- 200 The route.
- 400 A reference of 3 to 128 characters is required.
invalid_request
Request
curl -X GET "$KULPAY_URL/v1/admin/reference-routes" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"billers": [
{
"biller": "EDM",
"biller_name": "Electricidade de Moçambique",
"products": [
"CREDELEC"
]
},
{
"biller": "LUZ",
"biller_name": "Luz do Norte",
"products": [
"CONTA"
]
}
],
"reference": "01123456782",
"rf": false,
"then": "these billers, in parallel; the deposit resolver if none knows it",
"too_many": false
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "a reference is 3 to 128 characters",
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
GET /v1/admin/remittances
Remittances to billers
Newest first, of one biller or all. Requires reconciliation:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
biller | query | string |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/remittances" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"approval_id": "3KWPlDhVsFz6D7Nlw0sljeII8l1",
"bank_reference": "BCI-1",
"count": 1,
"created_at": "2026-10-10T22:14:59.538329669Z",
"currency": "MZN",
"fee_minor": 0,
"gross_minor": 150000,
"id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
"mode": "test",
"net_minor": 150000,
"paid_at": "2026-10-10T22:14:59.591566406Z",
"period_from": "2026-10-10T22:14:59.495269929Z",
"period_to": "2026-10-10T22:14:59.495269929Z",
"status": "paid"
}
],
"total": 1
}
POST /v1/admin/remittances/{id}/paid
Record a remittance as paid
The bank transfer that paid the biller: its reference and a reason. A second person approves; the biller payable is then cleared against client funds and the biller receives bill_remittance.paid. Requires reconciliation:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
bank_reference required | string | |
reason required | string |
Responses
- 202
{approval}. - 400 Already paid, or no reference.
invalid_request
Request
curl -X POST "$KULPAY_URL/v1/admin/remittances/{id}/paid" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"bank_reference": "BCI-1",
"reason": "daily"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-10T22:14:59.570339309Z",
"id": "3KWPlDhVsFz6D7Nlw0sljeII8l1",
"kind": "bill.remittance_paid",
"payload": "{\"bank_reference\":\"BCI-1\"}",
"prepared_by": "key_accounts",
"reason": "daily",
"status": "pending",
"subject_id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
"tenant_id": "platform"
}
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the remittance is already paid",
"error": {
"code": "invalid_request",
"message": "the remittance is already paid"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/admin/remittances/{id}/report.csv
A remittance report
Every payment remitted: KulPay id, date, invoice number, account reference, amount, fee, net; and the total. Requires reconciliation:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 CSV.
Request
curl -X GET "$KULPAY_URL/v1/admin/remittances/{id}/report.csv" \
-H "Authorization: Bearer $KULPAY_KEY"
GET /v1/admin/supply-connectors
Supply connectors
KulPay's own integrations with suppliers, with health, float and configuration (secrets masked). Requires connectors:read.
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/admin/supply-connectors" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"config": {
"float_minor": 5000000
},
"created_at": "2026-10-10T22:14:57.717491719Z",
"driver": "fake",
"enabled": true,
"float_at": "2026-10-10T22:14:57.742829025Z",
"float_currency": "MZN",
"float_minor": 5000000,
"health": "healthy",
"health_at": "2026-10-10T22:14:57.762013874Z",
"id": "3KWPkweeFBr3p9OxYa5dTFjzMCF",
"mode": "test",
"name": "fake-test",
"updated_at": "2026-10-10T22:14:57.717491719Z"
}
],
"total": 1
}
POST /v1/admin/supply-connectors
Add a supply connector
The configuration is envelope-encrypted; only its keys are recorded in bill_connector.configured. The fake driver is refused in live mode. Requires connectors:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
config | object | |
driver | string | |
enabled | boolean | |
mode | string | One of: live, test |
name | string |
Responses
- 201 The connector.
- 400
invalid_request.
Request
curl -X POST "$KULPAY_URL/v1/admin/supply-connectors" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"config": {
"float_minor": 5000000
},
"driver": "fake",
"mode": "test",
"name": "fake-test"
}'
201 Response application/json
{
"config": {
"float_minor": 5000000
},
"created_at": "2026-10-10T22:14:57.717491719Z",
"driver": "fake",
"enabled": true,
"health": "healthy",
"id": "3KWPkweeFBr3p9OxYa5dTFjzMCF",
"mode": "test",
"name": "fake-test",
"updated_at": "2026-10-10T22:14:57.717491719Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "mode is live or test",
"error": {
"code": "invalid_request",
"message": "mode is live or test"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
PATCH /v1/admin/supply-connectors/{name}
Change a supply connector
The configuration is merged into the stored one; a secret left out or sent empty keeps its value. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
name required | path | string | The supply connector name or ID. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
config | object | |
driver | string | |
enabled | boolean | |
mode | string | One of: live, test |
name | string |
Responses
- 200 The connector.
- 400
invalid_request. - 404 No such biller, product or supply connector.
not_found
Request
curl -X PATCH "$KULPAY_URL/v1/admin/supply-connectors/{name}" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"low_float_minor": 100000
}'
200 Response application/json
{
"config": {
"float_minor": 5000000
},
"created_at": "2026-10-10T22:14:57.717491719Z",
"driver": "fake",
"enabled": true,
"float_at": "2026-10-10T22:14:57.742829025Z",
"float_currency": "MZN",
"float_minor": 5000000,
"health": "healthy",
"health_at": "2026-10-10T22:14:57.762013874Z",
"id": "3KWPkweeFBr3p9OxYa5dTFjzMCF",
"low_float_minor": 100000,
"mode": "test",
"name": "fake-test",
"updated_at": "2026-10-10T22:14:57.992068663Z"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the fake driver is for test mode only",
"error": {
"code": "invalid_request",
"message": "the fake driver is for test mode only"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/supply-connectors/{name}/float-topups
Record a float top-up
Money treasury sent to a supplier for our float (BIL-3.4): the amount, the transfer's bank reference and a reason. A second person approves it (bill_float.topped_up); the next probe expects the supplier's float to have grown by it, and records bill_float.mismatch when it has not. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
name required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount_minor required | integer | |
bank_reference required | string | |
reason required | string |
Responses
- 202 The top-up,
pendingwith itsapproval_id. - 400
invalid_request. - 404 Unknown connector.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/supply-connectors/{name}/float-topups" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": 500000,
"bank_reference": "BCI-1",
"reason": "monthly"
}'
202 Response application/json
{
"amount_minor": 500000,
"approval_id": "3KWPlAON5BFKuGPEjNuGpBV06KB",
"bank_reference": "BCI-1",
"connector_id": "3KWPkweeFBr3p9OxYa5dTFjzMCF",
"created_at": "2026-10-10T22:14:58.017507082Z",
"created_by": "key_accounts",
"currency": "MZN",
"id": "3KWPl6P3YYG5B9DI0Klb9fMlSUu",
"status": "pending"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "a positive amount_minor and the bank_reference of the transfer are required",
"error": {
"code": "invalid_request",
"message": "a positive amount_minor and the bank_reference of the transfer are required"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/supply-connectors/{name}/probe
Probe a supply connector
Asks the supplier for its health and our float when the driver can tell (EDM INT03), and records them. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
name required | path | string | The supply connector name or ID. |
Responses
- 200 The connector.
- 404 No such biller, product or supply connector.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/supply-connectors/{name}/probe" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"config": {
"float_minor": 5000000
},
"created_at": "2026-10-10T22:14:57.717491719Z",
"driver": "fake",
"enabled": true,
"float_at": "2026-10-10T22:14:57.742829025Z",
"float_currency": "MZN",
"float_minor": 5000000,
"health": "healthy",
"health_at": "2026-10-10T22:14:57.762013874Z",
"id": "3KWPkweeFBr3p9OxYa5dTFjzMCF",
"mode": "test",
"name": "fake-test",
"updated_at": "2026-10-10T22:14:57.717491719Z"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/supply-connectors/{name}/reconcile
Reconcile a day with a supplier
Compares the payments KulPay sent the connector on day (YYYY-MM-DD, UTC; yesterday by default) with the supplier's records (BIL-6.3): its listing (EDM INT10, the biller contract's GET /payments?date=), or, with a text/csv body, its report file (columns request_id, supplier_reference, customer_reference, amount). Payments match by request id, else by receipt. Breaks: supplier_only, kulpay_only, amount_mismatch, review_found (the supplier delivered a payment KulPay has pending or in review), failed_but_delivered. Runs daily for connectors that list their payments (bill_recon.completed, bill_recon.break_found). Requires reconciliation:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
name required | path | string | |
day | query | string (date) |
Request body text/csv
Responses
- 200 The run with its breaks.
- 400 The driver cannot list its payments, or a bad file.
invalid_request - 404 Unknown connector.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/supply-connectors/{name}/reconcile" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"breaks": 2,
"breaks_list": [
{
"bill_payment_id": "3KUYJ7FUo4vHy1IKzBbOs5QmDw2",
"customer_reference": "07012345678",
"kind": "amount_mismatch",
"kulpay_amount_minor": 100000,
"request_id": "3KUYK1GVp7wIz4JLACcPt8RnEx3",
"supplier_amount_minor": 50000,
"supplier_reference": "EDM-20261010-558213"
},
{
"bill_payment_id": "3KUYL4HWq1xJA7KMBDdQu2SoFy4",
"customer_reference": "07019876543",
"kind": "kulpay_only",
"kulpay_amount_minor": 25000,
"request_id": "3KUYM7IXr4yKB1LNCEeRv5TpGz5",
"supplier_reference": "EDM-20261010-558990"
}
],
"connector": "edm_live",
"connector_id": "3KUXT4pDy6eRf8sUhJkYc2AwMg6",
"created_at": "2026-10-11T06:00:41Z",
"day": "2026-10-10T00:00:00Z",
"id": "3KUYI4ETn1uGx7HJyAaNr2PlCv1",
"kulpay_count": 1288,
"matched": 1286,
"source": "file",
"supplier_count": 1287
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the header needs amount and request_id or supplier_reference",
"error": {
"code": "invalid_request",
"message": "the header needs amount and request_id or supplier_reference"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/admin/supply-drivers
Supply drivers
The registered supply drivers with the shapes they serve and their configuration fields (secrets marked). fake simulates every shape in test mode; magic references ending 0000 (not found), 9991 (rejected), 9992 (ambiguous, then succeeded), 9993 (float exhausted), 9994 (ambiguous, never known), 9995 (slow), 9996 (unavailable). Requires connectors:read.
Responses
- 200
{data}.
Request
curl -X GET "$KULPAY_URL/v1/admin/supply-drivers" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"description": "Simulated supplier for test mode and the local stack; magic references choose the outcome.",
"fields": [
"float_minor",
"currency"
],
"name": "fake",
"shapes": [
"metered_prepaid",
"account_topup"
]
}
]
}
GET /v1/bill-commissions
Your commission statement
Your bill sales and commission for a month (YYYY-MM, Maputo time; this month by default), each sale with its commission. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
month | query | string |
Responses
- 200
{month, sales, volume, commission, lines}.
Request
curl -X GET "$KULPAY_URL/v1/bill-commissions" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"channel_id": "3KWPl9fd5z3WDdnaGzpvnzDkLUW",
"commission": "0.00",
"currency": "MZN",
"lines": [
{
"amount": "1500.00",
"bill_payment_id": "3KWPlDKa4bRtfAl70HOzBdgc2WY",
"biller": "AGUAS",
"commission": "0.00",
"completed_at": "2026-10-10T22:14:59.495269929Z",
"product": "FACTURA"
}
],
"month": "2026-10",
"sales": 1,
"volume": "1500.00"
}
GET /v1/bill-invoices/{id}/document
Download a bill's document
The document a biller attached to an invoice, for a channel that has a payer subscribed to that account or paid that invoice; anyone else gets 404. The payer's invoice list (GET /v1/payers/{id}/invoices) gives this path as document.url. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The invoice id. |
Responses
- 200 The PDF, as an attachment.
- 404 No such document for this channel.
not_found
Request
curl -X GET "$KULPAY_URL/v1/bill-invoices/{id}/document" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such invoice document",
"error": {
"code": "not_found",
"message": "no such invoice document"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/bill-invoices/{id}/engagements
Report what a payer did with a bill
A channel reports that its payer opened, pinned or unpinned an invoice. The biller is told of a payer's first open (bill_invoice.opened) and of a pin when it changes (bill_invoice.pinned, bill_invoice.unpinned); repeats are acknowledged with recorded: false and change nothing. The payer must be subscribed to the invoice's account through this channel; the biller never learns who they are, only an opaque payer_ref. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The invoice id. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
payer_id required | string | Your payer (id or external_id). |
surface | string | One of: app, web, ussd, agent, other |
type required | string | One of: opened, pinned, unpinned |
Responses
- 200 Nothing new (
recorded: false). - 201 Recorded: the biller is told.
- 400 Unknown type or surface, or the payer is not subscribed.
invalid_request - 404 Unknown invoice or payer.
not_found
Request
curl -X POST "$KULPAY_URL/v1/bill-invoices/{id}/engagements" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"payer_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"surface": "app",
"type": "opened"
}'
200 Response application/json
{
"recorded": false,
"type": "opened"
}
201 Response application/json
{
"recorded": true,
"type": "pinned"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the payer is not subscribed to this invoice's account",
"error": {
"code": "invalid_request",
"message": "the payer is not subscribed to this invoice's account"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/bill-invoices/{id}/qr.png
A bill's QR as PNG (channel)
The QR of a bill you may pay, to show your customer (BIL-5.12): it encodes the invoice's payment link (qr.url), which any phone camera opens. No personal data. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The invoice id. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 404 No such invoice, or one you may not pay.
not_found - 409
invoice_closed: a paid, cancelled or expired invoice has no QR.
Request
curl -X GET "$KULPAY_URL/v1/bill-invoices/{id}/qr.png" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
"code": "invoice_closed",
"detail": "invoice F-Q takes no payments, so it has no QR",
"error": {
"code": "invoice_closed",
"message": "invoice F-Q takes no payments, so it has no QR"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/bill-invoices/{id}/qr.svg
A bill's QR (channel)
The QR of a bill you may pay, to show your customer (BIL-5.12): it encodes the invoice's payment link (qr.url), which any phone camera opens. No personal data. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The invoice id. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 404 No such invoice, or one you may not pay.
not_found - 409
invoice_closed: a paid, cancelled or expired invoice has no QR.
Request
curl -X GET "$KULPAY_URL/v1/bill-invoices/{id}/qr.svg" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
"code": "invoice_closed",
"detail": "invoice F-Q takes no payments, so it has no QR",
"error": {
"code": "invoice_closed",
"message": "invoice F-Q takes no payments, so it has no QR"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/bill-payments
Your bill payments
Newest first, filtered by status, biller, customer_reference, up to limit (100, at most 500). Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | |
biller | query | string | |
customer_reference | query | string | |
limit | query | integer |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/bill-payments" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"amount": "250.00",
"artifacts": [
{
"type": "receipt",
"value": "FK2026101045631213"
},
{
"meta": {
"krn": "1",
"sgc": "600123",
"ti": "1"
},
"type": "token",
"unit_type": "kWh",
"units": "25.00",
"value": "69556183477675547957"
}
],
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.00",
"units": "25.00"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"completed_at": "2026-10-10T22:14:58.744497415Z",
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"funding": "balance",
"id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"partner_reference": "POS-9",
"payer_fee": "5.00",
"product": "CREDELEC",
"product_version": 1,
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"receipt_url": "/bills/receipt/3KWPl4pk0XxRI4TEVI6avRgrj1y?t=plain%3AYmlsbC1yZWNlaXB0fDNLV1BsNHBrMFh4Ukk0VEVWSTZhdlJncmoxeQ",
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"shape": "metered_prepaid",
"status": "succeeded",
"supplier_reference": "FK2026101045631213",
"total": "255.00"
}
],
"total": 1
}
POST /v1/bill-payments
Pay a quote
Pays a quote from your prefunded balance (BIL-2.4): the total is reserved, a request id is persisted and sent to the supplier. 201 with the payment succeeded (tokens, PINs or receipt in artifacts) or failed (nothing charged, the reserve released); 202 when the supplier has not answered within 25 seconds or gave no definitive answer: the payment is sending or pending, and KulPay queries the supplier with the same request id until it knows. Follow it with the bill_payment.succeeded, bill_payment.failed and bill_payment.pending webhooks or GET /v1/bill-payments/{id}; never pay again for the same purchase. Channels without a balance pay with funding: "collect" and collect: {method, phone}: the total is collected from the payer through the payment rails first (collection_id, bill_payment.collecting), the bill is sent once it succeeds, and a bill that then fails is refunded to the payer (bill_payment.refunded). A quote is paid as issued, through the supply connector that issued it, or not at all: a quote whose connector was disabled or withdrawn from the product, or whose terms no longer match, is refused before anything is reserved (409 quote_invalidated), and is never sent through another supplier; should that happen after acceptance but before sending, the payment fails with quote_invalidated and the reserve is released. payment_status (the money) and fulfilment (the supplier leg, with the evidence that settled it) are reported separately. A quote is paid once (409 quote_used); an expired one is refused (422 quote_expired); 422 insufficient_funds leaves the quote payable. Requires bills:write and an Idempotency-Key. The launch capability matrix (plan phase 10) holds the route first: one it does not enable is 422 route_not_enabled, and anything off_net 422 movement_scope_disabled, before any limit is reserved or provider called.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string (uuid) | Optional client-supplied UUID. The gateway caches the response for this key for the process lifetime; replays return the cached response. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
funding | string | One of: balance |
partner_reference | string | |
payer | object | |
payer.email | string (email) | |
payer.name | string | |
payer.phone | string | |
quote_id required | string |
Responses
- 201 Succeeded or failed.
- 202 Sending or pending.
- 404 No such quote.
not_found - 409
quote_used; orquote_invalidated: the quote no longer holds as issued (its supplier was disabled or withdrawn, or its terms changed). Request a new quote. - 422
quote_expired,insufficient_funds, orbiller_unavailable: the biller is not accepting payments. Or the route is not in the launch matrix (route_not_enabled, movement_scope_disabled). - 503
biller_unavailable: the supplier is down or not answering. It passes: retry afterRetry-Afterseconds.
Request
curl -X POST "$KULPAY_URL/v1/bill-payments" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"partner_reference": "POS-9",
"payer": {
"phone": "258841234567"
},
"quote_id": "3KWPl4pk0XxRI4TEVI6avRgrj1y"
}'
201 Response application/json
{
"accepted_at": "2026-10-10T22:14:58.70112Z",
"amount": "250.00",
"amount_minor": 25000,
"artifacts": [
{
"type": "receipt",
"value": "FK2026101045631213"
},
{
"meta": {
"krn": "1",
"sgc": "600123",
"ti": "1"
},
"type": "token",
"unit_type": "kWh",
"units": "25.00",
"value": "69556183477675547957"
}
],
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.00",
"units": "25.00"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"completed_at": "2026-10-10T22:14:58.744497415Z",
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"fulfilment": {
"attempts": 1,
"evidence": {
"authenticated_by": "connector_session",
"connector_id": "3KWPl3x9eQf0yX1oVJm2aQ8bLtR",
"driver": "edm",
"observed_at": "2026-10-10T22:14:58.744497415Z",
"outcome": "succeeded",
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"result_sha256": "5c1f0e6b2d9a4c7e8f3a1b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70",
"source": "fulfil_response",
"supplier_reference": "FK2026101045631213"
},
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"status": "succeeded",
"supplier_reference": "FK2026101045631213"
},
"funding": "balance",
"id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"partner_reference": "POS-9",
"payer_fee": "5.00",
"payer_fee_minor": 500,
"payment_status": "captured",
"product": "CREDELEC",
"product_version": 1,
"purchase_mode": "variable_amount",
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"receipt_url": "/bills/receipt/3KWPl4pk0XxRI4TEVI6avRgrj1y?t=plain%3AYmlsbC1yZWNlaXB0fDNLV1BsNHBrMFh4Ukk0VEVWSTZhdlJncmoxeQ",
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"shape": "metered_prepaid",
"status": "succeeded",
"supplier_connector": "edm-live",
"supplier_reference": "FK2026101045631213",
"total": "255.00",
"total_minor": 25500
}
202 Response application/json
{
"amount": "250.00",
"artifacts": [
{
"meta": {
"key": "1250.00"
},
"serial": "string",
"type": "token",
"unit_type": "string",
"units": "string",
"value": "1250.00"
}
],
"biller": "EDM",
"biller_name": "Electricidade de Moçambique",
"breakdown": {
"debt": "250.00",
"lines": [
{
"amount": "250.00",
"concept": "string",
"price": "string",
"units": "string"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"completed_at": "2026-10-10T09:30:00Z",
"created_at": "2026-10-10T09:30:00Z",
"currency": "MZN",
"customer_name": "Electricidade de Moçambique",
"customer_reference": "2003014567",
"failure_code": "EDM",
"failure_message": "Pagamento de energia, outubro",
"fee_bearer": "payer",
"funding": "balance",
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"mode": "test",
"object": "bill_payment",
"offer": "string",
"partner_reference": "2003014567",
"payer_fee": "250.00",
"product": "CREDELEC",
"product_version": 1,
"quote_expires_at": "2026-10-10T09:30:00Z",
"request_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"shape": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"status": "quoted",
"supplier_reference": "2003014567",
"total": "250.00"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · quote_used application/problem+json
{
"code": "quote_used",
"detail": "this quote was already paid (bill payment 3KWPl4pk0XxRI4TEVI6avRgrj1y is succeeded)",
"error": {
"code": "quote_used",
"message": "this quote was already paid (bill payment 3KWPl4pk0XxRI4TEVI6avRgrj1y is succeeded)"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
422 Error · insufficient_funds application/problem+json
{
"code": "insufficient_balance",
"detail": "the balance (10.00 MZN) does not cover 255.00 MZN",
"error": {
"code": "insufficient_funds",
"message": "the balance (10.00 MZN) does not cover 255.00 MZN"
},
"legacy_code": "insufficient_funds",
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
"code": "biller_unavailable",
"detail": "the supplier is not answering; try again shortly",
"error": {
"code": "biller_unavailable",
"message": "the supplier is not answering; try again shortly"
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
POST /v1/bill-payments/lookups
Look up a customer reference
Asks the product's supplier who a meter, account or number belongs to (BIL-2.2) before any money is taken. A found reference returns a lookup_id valid for 15 minutes, the customer's name masked, the amount due for postpaid references and the limits a payment must respect; a repeat within that time is answered from the lookup. status: not_found keeps nothing. 400 invalid_request when the reference does not match the product's pattern; 503 biller_unavailable when no supplier answers. Requires bills:read.
Request body application/json required
| Field | Type | Description |
|---|---|---|
biller required | string | |
customer_reference required | string | |
product required | string |
Responses
- 200 The lookup.
- 400
invalid_request. - 404 No such biller or product open to you.
not_found - 422
reference_rejected, orbiller_unavailable: the biller is not accepting payments (suspended or not yet live); a retry cannot succeed. - 503
biller_unavailable: the supplier is down or not answering. It passes: retry afterRetry-Afterseconds.
Request
curl -X POST "$KULPAY_URL/v1/bill-payments/lookups" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"biller": "EDM",
"customer_reference": "01234567890",
"product": "CREDELEC"
}'
200 Response application/json
{
"account": "AC01234567890",
"biller": "EDM",
"cached": false,
"constraints": {
"max": "50000.00",
"min": "10.00"
},
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"expires_at": "2026-10-10T22:29:58.620381112Z",
"lookup_id": "3KWPl4LcDleeGYpoU9aYN7alhT5",
"product": "CREDELEC",
"status": "found",
"tariff": "Prepaid Domestic 1PH"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "customer_reference is not a valid contador",
"error": {
"code": "invalid_request",
"message": "customer_reference is not a valid contador"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · reference_rejected application/problem+json
{
"code": "reference_rejected",
"detail": "Contador 100234567 não existe",
"error": {
"code": "reference_rejected",
"message": "Contador 100234567 não existe"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
"code": "biller_unavailable",
"detail": "the supplier is not answering; try again shortly",
"error": {
"code": "biller_unavailable",
"message": "the supplier is not answering; try again shortly"
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
POST /v1/bill-payments/quotes
Quote a bill payment
Prices a purchase at the product's supplier (BIL-2.3): name the customer with lookup_id or customer_reference (looked up first when the product requires it) and the value with amount or offer. The quote has the amount, the payer's fee (when the payer bears it), the total to collect, the supplier's breakdown (units, debt, lines) and quote_expires_at (at most 10 minutes). It is pinned to the supply connector (supplier_connector), the product version and the product's code at the supplier that produced it, with a digest of its terms: it is paid as issued or not at all. purchase_mode says whether it was priced from the catalogue (offer) or as an amount; a product may sell either or both (purchase_modes on the listing). To price it again, refresh it (POST /v1/bill-payments/quotes/{id}/refresh): a new quote from the same connector, the old one unchanged. 422 amount_out_of_range gives the allowed range. Requires bills:write. When the provider states them, the quote also carries the bill's open amount (outstanding_minor) and whether it may be paid in part (allow_partial), as stated at quote time. When the provider describes the bill, the quote carries bill_details (holder, service address, billing period, issue and due dates) for display; they are not terms.
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount | string | |
biller required | string | |
customer_reference | string | |
lookup_id | string | |
offer | string | An offer code from the product's catalogue (`fixed_catalog`); leave out for an amount (`variable_amount`). |
product required | string |
Responses
- 201 The quote, a bill payment in
quoted. - 400
invalid_request. - 404 No such biller or product open to you.
not_found - 422
amount_out_of_range,reference_not_found,reference_rejected; orbiller_unavailable: the biller is not accepting payments; a retry cannot succeed. - 503
biller_unavailable: the supplier is down or not answering. It passes: retry afterRetry-Afterseconds.
Request
curl -X POST "$KULPAY_URL/v1/bill-payments/quotes" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "250",
"biller": "EDM",
"lookup_id": "3KWPl4LcDleeGYpoU9aYN7alhT5",
"product": "CREDELEC"
}'
201 Response application/json
{
"amount": "250.00",
"amount_minor": 25000,
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.00",
"units": "25.00"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"fulfilment": {
"attempts": 0,
"status": "not_started"
},
"id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"payer_fee": "5.00",
"payer_fee_minor": 500,
"payment_status": "none",
"product": "CREDELEC",
"product_version": 1,
"purchase_mode": "variable_amount",
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"shape": "metered_prepaid",
"status": "quoted",
"supplier_connector": "edm-live",
"total": "255.00",
"total_minor": 25500
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "amount must be a positive decimal",
"error": {
"code": "invalid_request",
"message": "amount must be a positive decimal"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · amount_out_of_range application/problem+json
{
"code": "amount_out_of_range",
"detail": "the amount must be between 20.00 MZN and 50000.00 MZN",
"error": {
"code": "amount_out_of_range",
"message": "the amount must be between 20.00 MZN and 50000.00 MZN"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
"code": "biller_unavailable",
"detail": "the supplier is not answering; try again shortly",
"error": {
"code": "biller_unavailable",
"message": "the supplier is not answering; try again shortly"
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
POST /v1/bill-payments/quotes/{id}/refresh
Refresh a bill quote
Prices a quote again at the supply connector that issued it (design: a refresh creates a new quotation) and returns a new quote, with refreshed_from naming the old one. The old quote is never changed, whatever its state (quoted, expired, paid): what a payer accepted stays as accepted. The reference is looked up again at the same connector when the product requires it; the offer or amount is the old quote's. If that connector is disabled or no longer sells the product, the refresh is refused (409 quote_invalidated or 503 biller_unavailable) rather than priced elsewhere: request a new quote instead. An invoice is quoted again with invoice_id. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The quote (bill payment) to refresh. |
Responses
- 201 The new quote, a bill payment in
quoted. - 400
invalid_request: an invoice quote (quote the invoice again withinvoice_id). - 404 No such quote of yours.
not_found - 409
quote_invalidated: the connector that issued the quote no longer sells the product. Request a new quote. - 422
amount_out_of_range,reference_not_found,reference_rejected; orbiller_unavailable: the biller is not accepting payments; a retry cannot succeed. - 503
biller_unavailable: the supplier is down or not answering. It passes: retry afterRetry-Afterseconds.
Request
curl -X POST "$KULPAY_URL/v1/bill-payments/quotes/{id}/refresh" \
-H "Authorization: Bearer $KULPAY_KEY"
201 Response application/json
{
"amount": "250.00",
"amount_minor": 25000,
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.42",
"units": "24.00"
}
],
"unit_type": "kWh",
"units": "24.00"
},
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"fulfilment": {
"attempts": 0,
"status": "not_started"
},
"id": "3KWPl9Qy0bWm2ZkT4dNfE1sH7aP",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"payer_fee": "5.00",
"payer_fee_minor": 500,
"payment_status": "none",
"product": "CREDELEC",
"product_version": 1,
"purchase_mode": "variable_amount",
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"refreshed_from": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"shape": "metered_prepaid",
"status": "quoted",
"supplier_connector": "edm-live",
"total": "255.00",
"total_minor": 25500
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "an invoice is quoted again with invoice_id",
"error": {
"code": "invalid_request",
"message": "an invoice is quoted again with invoice_id"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · quote_invalidated application/problem+json
{
"code": "quote_invalidated",
"detail": "the supplier that issued this quote no longer sells Credelec; request a new quote",
"error": {
"code": "quote_invalidated",
"message": "the supplier that issued this quote no longer sells Credelec; request a new quote"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
422 Error · amount_out_of_range application/problem+json
{
"code": "amount_out_of_range",
"detail": "the amount must be between 20.00 MZN and 50000.00 MZN",
"error": {
"code": "amount_out_of_range",
"message": "the amount must be between 20.00 MZN and 50000.00 MZN"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
"code": "biller_unavailable",
"detail": "the supplier is not answering; try again shortly",
"error": {
"code": "biller_unavailable",
"message": "the supplier is not answering; try again shortly"
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
GET /v1/bill-payments/{id}
A bill payment
With its artifacts once succeeded. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 The bill payment.
- 404 Not yours or unknown.
not_found
Request
curl -X GET "$KULPAY_URL/v1/bill-payments/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"amount": "250.00",
"artifacts": [
{
"type": "receipt",
"value": "FK2026101045631213"
},
{
"meta": {
"krn": "1",
"sgc": "600123",
"ti": "1"
},
"type": "token",
"unit_type": "kWh",
"units": "25.00",
"value": "69556183477675547957"
}
],
"biller": "EDM",
"biller_name": "EDM",
"breakdown": {
"lines": [
{
"amount": "250.00",
"concept": "Unit charge per kWh",
"price": "10.00",
"units": "25.00"
}
],
"unit_type": "kWh",
"units": "25.00"
},
"completed_at": "2026-10-10T22:14:58.744497415Z",
"created_at": "2026-10-10T22:14:58.651626029Z",
"currency": "MZN",
"customer_name": "C****** T**** 7***",
"customer_reference": "01234567890",
"fee_bearer": "payer",
"funding": "balance",
"id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
"mode": "test",
"object": "bill_payment",
"participants": [
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "initiating_psp"
},
{
"account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
"name": "Tenant B",
"role": "from_psp",
"settlement": "prefunded_balance"
}
],
"partner_reference": "POS-9",
"payer_fee": "5.00",
"product": "CREDELEC",
"product_version": 1,
"quote_expires_at": "2026-10-10T22:24:58.651608397Z",
"receipt_url": "/bills/receipt/3KWPl4pk0XxRI4TEVI6avRgrj1y?t=plain%3AYmlsbC1yZWNlaXB0fDNLV1BsNHBrMFh4Ukk0VEVWSTZhdlJncmoxeQ",
"request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
"shape": "metered_prepaid",
"status": "succeeded",
"supplier_reference": "FK2026101045631213",
"total": "255.00"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/bill-payments/{id}/receipt.pdf
A printable receipt
The channel's own bill payment as a PDF to keep or print: biller, product, reference, amounts, dates, status, the KulPay and supplier references, and the tokens and PINs with their units and serials. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The bill payment id. |
Responses
- 200 The PDF, as an attachment.
- 404 Unknown.
not_found
Request
curl -X GET "$KULPAY_URL/v1/bill-payments/{id}/receipt.pdf" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/bill-payments/{id}/resend
Send the payer their token or receipt again
Sends a succeeded payment's token, PIN or receipt to the payer again (BIL-2.6), by email (through the notification outbox) or sms (503 sms_unavailable when the gateway has no SMS channel), to to or the contact given when paying. At most three times per payment (422 resend_limit). The payment also carries receipt_url, a signed hosted receipt that shows the token once the payer confirms the phone number given when paying. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
to | string | |
via required | string | One of: sms, email |
Responses
- 202
{via, to}, the recipient masked. - 400
invalid_request. - 404 Not yours or unknown.
not_found - 422
resend_limit. - 503
sms_unavailable.
Request
curl -X POST "$KULPAY_URL/v1/bill-payments/{id}/resend" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "anaexample.com",
"via": "email"
}'
202 Response application/json
{
"to": "***********.com",
"via": "email"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "via is sms or email",
"error": {
"code": "invalid_request",
"message": "via is sms or email"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such bill payment or quote",
"error": {
"code": "not_found",
"message": "no such bill payment or quote"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · resend_limit application/problem+json
{
"code": "resend_limit",
"detail": "the artifacts were already sent again 3 times",
"error": {
"code": "resend_limit",
"message": "the artifacts were already sent again 3 times"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
503 Error · sms_unavailable application/problem+json
{
"code": "sms_unavailable",
"detail": "this gateway has no SMS channel configured; send by email",
"error": {
"code": "sms_unavailable",
"message": "this gateway has no SMS channel configured; send by email"
},
"status": 503,
"title": "Service Unavailable",
"type": "about:blank"
}
GET /v1/biller/accounts/{ref}/qr
A customer account's QR (biller)
A QR for one of your customer accounts (BIL-5.13): print it once (on the contract, a bill, a sticker) and your customer pays every month. It encodes a plain https link any phone camera opens; the page shows what is due on the account now (its open invoices, oldest due first), each paid on its invoice page. A partner's app that reads it resolves it (POST /v1/qr/resolve) and pays in the app. In the mode of your key. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
ref required | path | string | The customer account or meter number: 1 to 40 printable characters, no spaces. |
product | query | string | The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it. |
Responses
- 200 The account's QR.
- 400
invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active. - 404 No such biller.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/accounts/{ref}/qr" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"biller": "AGUAS",
"mode": "live",
"png_url": "/v1/biller/accounts/CL-001234/qr.png",
"reference": "CL-001234",
"svg_url": "/v1/biller/accounts/CL-001234/qr.svg",
"url": "https://pay.kulpay.co.mz/pay/account/bB9BR1VBUx8fQ0wtMDA0Mg.3oT2Lw9sQe1Zk7mN"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no product of AGUAS takes CL-****2 as a reference",
"error": {
"code": "invalid_request",
"message": "no product of AGUAS takes CL-****2 as a reference"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/accounts/{ref}/qr.png
A customer account's QR, PNG (biller)
A QR for one of your customer accounts (BIL-5.13): print it once (on the contract, a bill, a sticker) and your customer pays every month. It encodes a plain https link any phone camera opens; the page shows what is due on the account now (its open invoices, oldest due first), each paid on its invoice page. A partner's app that reads it resolves it (POST /v1/qr/resolve) and pays in the app. In the mode of your key. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
ref required | path | string | The customer account or meter number: 1 to 40 printable characters, no spaces. |
product | query | string | The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 400
invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active. - 404 No such biller.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/accounts/{ref}/qr.png" \
-H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no product of AGUAS takes CL-****2 as a reference",
"error": {
"code": "invalid_request",
"message": "no product of AGUAS takes CL-****2 as a reference"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/accounts/{ref}/qr.svg
A customer account's QR, rendered (biller)
A QR for one of your customer accounts (BIL-5.13): print it once (on the contract, a bill, a sticker) and your customer pays every month. It encodes a plain https link any phone camera opens; the page shows what is due on the account now (its open invoices, oldest due first), each paid on its invoice page. A partner's app that reads it resolves it (POST /v1/qr/resolve) and pays in the app. In the mode of your key. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
ref required | path | string | The customer account or meter number: 1 to 40 printable characters, no spaces. |
product | query | string | The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 400
invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active. - 404 No such biller.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/accounts/{ref}/qr.svg" \
-H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "no product of AGUAS takes CL-****2 as a reference",
"error": {
"code": "invalid_request",
"message": "no product of AGUAS takes CL-****2 as a reference"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/biller/invoice-files
Upload an invoice file
A biller that presents by file sends a CSV (BIL-5.3) with the header invoice_number, account_reference, amount, currency, issue_date, due_date, description (issue_date and description optional), at most 10,000 rows and 5 MB. The file is checked as a whole, then loaded row by row as POST /v1/biller/invoices would; loading it again updates the unpaid invoices rather than duplicating them. Answers with the counts and a result per row, or the result file with ?format=csv. Records bill_invoice.file_loaded. Requires invoices:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
format | query | string | One of: csv |
Request body text/csv required
Responses
- 200
{sha256, rows, created, updated, rejected, results}, or the result CSV. - 400 Not a valid file, or not set up to present by file.
invalid_request - 413
file_too_large.
Request
curl -X POST "$KULPAY_URL/v1/biller/invoice-files" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"created": 1,
"rejected": 1,
"results": [
{
"invoice_id": "3KWPlG2Ons62fPH0IBShlpPJm9n",
"number": "G-1",
"result": "created"
},
{
"error": "amount must be a positive decimal",
"number": "G-2",
"result": "rejected"
}
],
"rows": 2,
"sha256": "90868a50e8b9c3b766597f54aa00e65c587cda5f7454803e5e8a4fb38d9fad00",
"updated": 0
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the header must have invoice_number, account_reference, amount, currency, issue_date, due_date, description",
"error": {
"code": "invalid_request",
"message": "the header must have invoice_number, account_reference, amount, currency, issue_date, due_date, description"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
413 Error · file_too_large application/problem+json
{
"code": "file_too_large",
"detail": "an invoice file is at most 5 MB",
"error": {
"code": "file_too_large",
"message": "an invoice file is at most 5 MB"
},
"status": 413,
"title": "Content Too Large",
"type": "about:blank"
}
GET /v1/biller/invoices
Your invoices
Filtered by status and account_reference, by due date. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status | query | string | One of: open, partially_paid, paid, cancelled, expired |
account_reference | query | string |
Responses
- 200
{data, total}: each with amount, paid and open.
Request
curl -X GET "$KULPAY_URL/v1/biller/invoices" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"account_reference": "CL-1",
"amount": "1500.00",
"biller": "AGUAS",
"biller_name": "Águas",
"currency": "MZN",
"due_date": "2026-10-31",
"expires_at": "2026-11-30T00:00:00Z",
"id": "3KWPlCZORCtehfKOAIkXyowwAqr",
"issue_date": "2026-10-10",
"number": "F-1",
"open": "0.00",
"paid": "1500.00",
"status": "paid"
}
],
"total": 1
}
POST /v1/biller/invoices
Present invoices
A biller that presents by push sends its invoices (BIL-5.2): one invoice, or {invoices: [...]} up to 1,000, answered per invoice (created, updated, rejected with the reason). The number is unique per biller; presenting it again updates an unpaid invoice. Lines, when given, must add up to the amount. Channels pay an invoice by quoting with invoice_id (POST /v1/bill-payments/quotes): the amount defaults to what is still open; a partial or larger payment only when the biller accepts it. When a payment succeeds the biller receives bill_invoice.paid. 403 not_a_biller when the key's organisation is not a biller. Requires invoices:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
account_reference required | string | |
amount required | string | |
currency required | string | |
description | string | |
due_date required | string (date) | |
issue_date | string (date) | |
lines | object[] | |
lines[].amount | string | |
lines[].description | string | |
number required | string | |
pay_url | string | The invoice's payment link, while it takes payments (BIL-5.9); reissue it with POST /v1/biller/invoices/{number}/payment-link. |
payment_reference | string | The ISO 11649 reference (RF…, grouped by four) a payer quotes to pay at a partner bank or PSP counter or by transfer (BIL-5.8). |
pdf_url | string (uri) | |
qr | object | Its QR while it takes payments (BIL-5.12), on a read of one invoice: `url` is the invoice's payment link, which any phone camera opens; render it, or use the rendered images. |
qr.png_url | string | |
qr.svg_url | string | |
qr.url | string |
Responses
- 200
{results, created, updated, rejected}. - 400
invalid_request: not set up to present by push, or an empty batch. - 403
not_a_biller.insufficient_scope
Request
curl -X POST "$KULPAY_URL/v1/biller/invoices" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_reference": "CL-9",
"amount": "300",
"currency": "MZN",
"due_date": "2026-12-31",
"number": "F-9"
}'
200 Response application/json
{
"created": 2,
"rejected": 1,
"results": [
{
"invoice_id": "3KWPlCZORCtehfKOAIkXyowwAqr",
"number": "F-1",
"result": "created"
},
{
"invoice_id": "3KWPlGyWnT4jx8g7CGpEoWP0GHe",
"number": "F-2",
"result": "created"
}
],
"updated": 0
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "send 1 to 1000 invoices",
"error": {
"code": "invalid_request",
"message": "send 1 to 1000 invoices"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
403 Error · insufficient_scope application/problem+json
{
"code": "insufficient_scope",
"detail": "missing required scope: invoices:write",
"error": {
"code": "insufficient_scope",
"message": "missing required scope: invoices:write",
"missing_scope": "invoices:write"
},
"missing_scope": "invoices:write",
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
GET /v1/biller/invoices/{number}
One of your invoices
Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 200 The invoice.
- 404 Unknown.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_reference": "CL-1",
"amount": "1500.00",
"biller": "AGUAS",
"biller_name": "Águas",
"currency": "MZN",
"due_date": "2026-10-31",
"engagement": {
"opened": 0,
"pinned": 0
},
"expires_at": "2026-11-30T00:00:00Z",
"id": "3KWPlCZORCtehfKOAIkXyowwAqr",
"issue_date": "2026-10-10",
"number": "F-1",
"open": "1500.00",
"paid": "0.00",
"status": "open"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PATCH /v1/biller/invoices/{number}
Change or cancel an invoice
The amount or due date of an unpaid invoice, or status: cancelled. A paid or partly paid invoice cannot be changed (409 invoice_locked). Requires invoices:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
amount | string | |
due_date | string (date) | |
status | string | One of: cancelled |
Responses
- 200 The invoice.
- 404 Unknown.
not_found - 409
invoice_locked.
Request
curl -X PATCH "$KULPAY_URL/v1/biller/invoices/{number}" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "cancelled"
}'
200 Response application/json
{
"account_reference": "CL-2",
"amount": "900.00",
"biller": "AGUAS",
"biller_name": "Águas",
"currency": "MZN",
"due_date": "2026-10-31",
"expires_at": "2026-11-30T00:00:00Z",
"id": "3KWPlGyWnT4jx8g7CGpEoWP0GHe",
"issue_date": "2026-10-10",
"number": "F-2",
"open": "900.00",
"paid": "0.00",
"status": "cancelled"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_locked application/problem+json
{
"code": "invoice_locked",
"detail": "invoice F-1 is paid and cannot be changed",
"error": {
"code": "invoice_locked",
"message": "invoice F-1 is paid and cannot be changed"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/biller/invoices/{number}/document
Download an invoice's document
Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 200 The PDF, as an attachment.
- 404 No document.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}/document" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PUT /v1/biller/invoices/{number}/document
Attach an invoice's document
The body is the PDF (Content-Type: application/pdf, starting %PDF-, at most 5 MB); ?filename= names it. Replaces any previous document. A cancelled or expired invoice takes none (409 invoice_closed). Channels download it from the gateway. Requires invoices:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
filename | query | string |
Request body application/pdf required
Responses
- 200 The invoice, with its
document. - 400 Not a PDF, or empty.
invalid_request - 404 Unknown invoice.
not_found - 409
invoice_closed. - 413
file_too_large.
Request
curl -X PUT "$KULPAY_URL/v1/biller/invoices/{number}/document" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_reference": "100234567",
"amount": "1250.00",
"biller": "AGUA-LIMPA",
"biller_name": "Água Limpa, Lda",
"currency": "MZN",
"description": "Consumo de água, setembro de 2026",
"document": {
"content_type": "application/pdf",
"filename": "FT-2026-00123.pdf",
"size": 84213,
"url": "/v1/biller/invoices/FT-2026-00123/document"
},
"due_date": "2026-10-31",
"expires_at": "2026-11-30T00:00:00Z",
"id": "3KnE2odsld9qpNsTIAS3N9RyYRi",
"issue_date": "2026-10-01",
"number": "FT-2026-00123",
"open": "1250.00",
"paid": "0.00",
"status": "open"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the document must be a PDF",
"error": {
"code": "invalid_request",
"message": "the document must be a PDF"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
"code": "invoice_closed",
"detail": "invoice FT-2026-00123 is paid: it takes no document",
"error": {
"code": "invoice_closed",
"message": "invoice FT-2026-00123 is paid: it takes no document"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
413 Error · file_too_large application/problem+json
{
"code": "file_too_large",
"detail": "an invoice document is at most 5 MB",
"error": {
"code": "file_too_large",
"message": "an invoice document is at most 5 MB"
},
"status": 413,
"title": "Content Too Large",
"type": "about:blank"
}
DELETE /v1/biller/invoices/{number}/document
Remove an invoice's document
Requires invoices:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 204 Removed.
- 404 No document.
not_found
Request
curl -X DELETE "$KULPAY_URL/v1/biller/invoices/{number}/document" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/invoices/{number}/events
An invoice's timeline
Every event the biller was sent about one of its invoices, oldest first, exactly as its webhooks carried them: data.invoice (the invoice after the event, the same in every event) and data.details (what is specific to the event). The biller-facing invoice events are bill_invoice.presented, bill_invoice.updated, bill_invoice.cancelled, bill_invoice.expired, bill_invoice.overdue, bill_invoice.document_attached, bill_invoice.document_removed, bill_invoice.sent, bill_invoice.reminder_sent, bill_invoice.opened, bill_invoice.pinned, bill_invoice.unpinned, bill_invoice.payment_received, bill_invoice.partially_paid, bill_invoice.paid, bill_invoice.remitted. Payment events name their participants and roles: initiating_psp (the channel paid through), from_psp (where the money came from: the channel's prefunded balance, or the payer's wallet or bank) and to_psp (where it goes for the biller: its bank by remittance, or KulPay for supplier-float products). Payers are named only by payer_ref, an opaque id stable per biller and payer. Delivery is at least once and may be out of order: process each event id once, and order an invoice's events by sequence. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 200
{data: [{id, type, sequence, created_at, data}], total}. - 404 Unknown invoice.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}/events" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"created_at": "2026-10-01T06:00:12Z",
"data": {
"details": {
"has_document": false,
"lines_count": 0,
"source": "push"
},
"invoice": {
"account_reference": "100234567",
"amount": "1250.00",
"biller": "AGUA-LIMPA",
"currency": "MZN",
"due_date": "2026-10-31",
"id": "3KnE2odsld9qpNsTIAS3N9RyYRi",
"issue_date": "2026-10-01",
"number": "FT-2026-00123",
"open": "1250.00",
"paid": "0.00",
"status": "open"
}
},
"id": "3KM4b0MSzyoNnAgn5pbS9xwrBQo",
"sequence": 1,
"type": "bill_invoice.presented"
},
{
"created_at": "2026-10-01T06:02:40Z",
"data": {
"details": {
"file_id": "3KRlP4WSt8uUE2E2OYdHrWSK4QF",
"filename": "FT-2026-00123.pdf",
"size": 84213
},
"invoice": {
"account_reference": "100234567",
"amount": "1250.00",
"biller": "AGUA-LIMPA",
"currency": "MZN",
"due_date": "2026-10-31",
"id": "3KnE2odsld9qpNsTIAS3N9RyYRi",
"issue_date": "2026-10-01",
"number": "FT-2026-00123",
"open": "1250.00",
"paid": "0.00",
"status": "open"
}
},
"id": "3KjRgUjLWIWhBmLa6Z9fWbpx2xe",
"sequence": 2,
"type": "bill_invoice.document_attached"
}
],
"total": 2
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/biller/invoices/{number}/payment-link
Reissue an invoice's payment link
Replaces the invoice's payment link (BIL-5.9): the old link stops working at once. Every invoice gets a link when presented (pay_url, while it takes payments) and an ISO 11649 payment reference (payment_reference, RF…) a payer quotes at a partner bank or PSP (BIL-5.8). Records bill_invoice.payment_link_issued. The link's payments run in this key's mode. Requires invoices:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 200 The invoice, with its new
pay_url. - 404 No such invoice.
not_found - 409
invoice_closed: the invoice takes no more payments.
Request
curl -X POST "$KULPAY_URL/v1/biller/invoices/{number}/payment-link" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"number": "FT-2026-0412",
"pay_url": "https://pay.kulpay.co.mz/pay/invoice/vY1q8m2Kp9sXc3RtLwB7nZ4hJ6fD0aE5gU8iO2yT1kM",
"payment_reference": "RF47 1234 5678 9012",
"status": "open"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
"code": "invoice_closed",
"detail": "invoice FT-2026-0412 is paid",
"error": {
"code": "invoice_closed",
"message": "invoice FT-2026-0412 is paid"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/biller/invoices/{number}/qr.png
An invoice's QR as PNG (biller)
The QR to print on your invoice (BIL-5.12): it encodes the invoice's payment link (qr.url), a plain https link any phone camera opens; the page shows the invoice and takes the payment, and a partner's app that reads it pays it in the app. No personal data. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 404 No such invoice.
not_found - 409
invoice_closed: a paid, cancelled or expired invoice has no QR.
Request
curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}/qr.png" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
"code": "invoice_closed",
"detail": "invoice F-Q takes no payments, so it has no QR",
"error": {
"code": "invoice_closed",
"message": "invoice F-Q takes no payments, so it has no QR"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/biller/invoices/{number}/qr.svg
An invoice's QR (biller)
The QR to print on your invoice (BIL-5.12): it encodes the invoice's payment link (qr.url), a plain https link any phone camera opens; the page shows the invoice and takes the payment, and a partner's app that reads it pays it in the app. No personal data. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required | path | string | The biller's invoice number. |
Responses
- 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
- 404 No such invoice.
not_found - 409
invoice_closed: a paid, cancelled or expired invoice has no QR.
Request
curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}/qr.svg" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
"code": "invoice_closed",
"detail": "invoice F-Q takes no payments, so it has no QR",
"error": {
"code": "invoice_closed",
"message": "invoice F-Q takes no payments, so it has no QR"
},
"status": 409,
"title": "Conflict",
"type": "about:blank"
}
GET /v1/biller/payers
Your enrolled customers
Contacts masked. Requires payers:write.
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/biller/payers" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"account_reference": "CL-10",
"created_at": "2026-10-10T22:14:59.775211958Z",
"lawful_basis": "contract",
"phone_masked": "*****4567"
},
{
"account_reference": "CL-9",
"created_at": "2026-10-10T22:14:59.755120163Z",
"lawful_basis": "contract",
"phone_masked": "*****4567"
}
],
"total": 2
}
POST /v1/biller/payers
Enrol your customers
A biller registers its customers so they receive its bills on KulPay channels (BIL-5.5): lawful_basis (consent, contract, legal_obligation, legitimate_interest) and up to 1,000 payers with an account reference and a phone number or email. Each is matched to channels' payers by phone or email; matched payers are subscribed (their channel is told and can let them opt out), except those who opted out of that account. Answers per customer with payers_subscribed. Requires payers:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
lawful_basis required | string | One of: consent, contract, legal_obligation, legitimate_interest |
payers required | object[] | |
payers[].account_reference required | string | |
payers[].email | string | |
payers[].name | string | |
payers[].phone | string |
Responses
- 200
{results}. - 403
not_a_biller.
Request
curl -X POST "$KULPAY_URL/v1/biller/payers" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"lawful_basis": "contract",
"payers": [
{
"account_reference": "CL-9",
"phone": "841234567"
},
{
"account_reference": "CL-10",
"phone": "841234567"
}
]
}'
200 Response application/json
{
"results": [
{
"account_reference": "CL-9",
"payers_subscribed": 0,
"result": "enrolled"
},
{
"account_reference": "CL-10",
"payers_subscribed": 1,
"result": "enrolled"
}
]
}
403 Error · not_a_biller application/problem+json
{
"code": "not_a_biller",
"detail": "this key's organisation is not a biller on KulPay",
"error": {
"code": "not_a_biller",
"message": "this key's organisation is not a biller on KulPay"
},
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
DELETE /v1/biller/payers/{ref}
Remove an enrolment
The subscriptions it created end (bill_subscription.ended) and its data is deleted after 30 days. Requires payers:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
ref required | path | string | The account reference. |
Responses
- 204 Removed.
- 404 No active enrolment.
not_found
Request
curl -X DELETE "$KULPAY_URL/v1/biller/payers/{ref}" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/products
Your products (biller)
Your products as KulPay sells them, with what you integrate against (BIL-5.15): supplier_product, the code your endpoints receive as product (the one you gave, else the product's own code; never empty), the reference each takes, whether lookups are routed to it, the fee you bear, and the connectors (endpoints) serving it. Requires invoices:read.
Responses
- 200 Your products.
- 403 Not a biller's key.
not_a_biller
Request
curl -X GET "$KULPAY_URL/v1/biller/products" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"amount_rule": "range",
"biller_fee": {
"bearer": "biller",
"currency": "MZN",
"fixed": "5.00"
},
"code": "CONTA",
"connectors": [
{
"driver": "biller_rest",
"health": "healthy",
"mode": "live",
"name": "aguas-live"
}
],
"currency": "MZN",
"customer_reference": {
"example": "EX-123456",
"label": "Contrato",
"pattern": "^EX-\\d{6}$"
},
"enabled": true,
"fee": {
"bearer": "biller",
"currency": "MZN"
},
"lookup_required": true,
"max": "50000.00",
"min": "1.00",
"model": "agent",
"name": "Conta de água",
"purchase_modes": [
"variable_amount"
],
"routable": true,
"shape": "postpaid",
"supplier_product": "AGUA_DOM",
"version": 2
}
]
}
403 Error · not_a_biller application/problem+json
{
"code": "not_a_biller",
"detail": "this key is not a biller's",
"error": {
"code": "not_a_biller",
"message": "this key is not a biller's"
},
"status": 403,
"title": "Forbidden",
"type": "about:blank"
}
GET /v1/biller/products/{product}/integration
A product's notification and report (biller)
The product's own integration with its LSP in a mode (BIL-5.16), secrets masked; defaults when none was set. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
Responses
- 200 The integration.
- 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/products/{product}/integration" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"biller": "AGUAS",
"fields": [
"event",
"notification_id",
"payment_id",
"request_id",
"mode",
"biller",
"biller_name",
"product",
"supplier_product",
"reference",
"invoice_number",
"amount",
"payer_fee",
"total",
"currency",
"amount_minor",
"total_minor",
"paid_at",
"channel_id",
"channel_name",
"funding",
"partner_reference",
"supplier_reference",
"receipt_url"
],
"mode": "test",
"notification": {
"auth": "hmac",
"content_type": "application/xml",
"enabled": true,
"headers": {
"X-Biller-Code": "AGUAS-01"
},
"method": "POST",
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
"url": "https://lsp.example/kulpay/pagamentos"
},
"product": "FACTURA",
"report": {
"auth": "bearer",
"columns": [
{
"field": "paid_at",
"header": "DATA",
"width": 10
},
{
"field": "invoice_number",
"header": "FACTURA",
"width": 12
},
{
"align": "right",
"field": "total",
"header": "VALOR",
"width": 10
}
],
"date_format": "DD/MM/YYYY",
"decimal_separator": ",",
"enabled": true,
"file_name": "AGUAS_{date}.dat",
"format": "txt",
"header": true,
"last_day": "2026-10-19",
"line_ending": "lf",
"url": "https://lsp.example/kulpay/reconciliacao"
},
"updated_at": "2026-10-20T08:00:00Z",
"version": 3
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PUT /v1/biller/products/{product}/integration
Set a product's notification and report (biller)
Sets the payment notification (URL, body template, content type, auth, headers) and the daily report (format, columns, file name, URL) of a product in a mode (BIL-5.16). Fields left out keep their value. Each change is a new version (bill_product.integration_set). The change takes effect at once, in live too, with no second approval (owner decision 2026-10-20). Notification and report URLs follow the webhook URL rules: absolute https, no credentials, at most 500 characters, never a link-local, multicast or reserved address. Requires webhooks:manage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
Request body application/json required
| Field | Type | Description |
|---|---|---|
notification | object | The payment notification: every payment of the product that succeeds is sent to `url`, its body rendered from `template` (Go text/template over the fields; helpers `json`, `xml`, `url` escape a value, `date "DD/MM/YYYY HH:mm"` formats paid_at, `cents` drops the decimal point). Checked when set: the body must be what `content_type` says. Retried for 24 h; kept for resends. |
notification.auth | object | How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only. |
notification.auth.password | string | |
notification.auth.secret | string | |
notification.auth.token | string | |
notification.auth.type | string | One of: none, bearer, basic, hmac |
notification.auth.username | string | |
notification.content_type | string | One of: application/json, application/xml, application/x-www-form-urlencoded, text/plain |
notification.enabled | boolean | |
notification.headers | object | Up to 10 static headers; not Authorization, Content-Type or KulPay-*. |
notification.method | string | One of: POST, PUT |
notification.template | string | |
notification.url | string (uri) | |
report | object | The daily report: the product's payments of a day, in the LSP's format and file extension; downloadable, and sent to `url` the next day. |
report.auth | object | How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only. |
report.auth.password | string | |
report.auth.secret | string | |
report.auth.token | string | |
report.auth.type | string | One of: none, bearer, basic, hmac |
report.auth.username | string | |
report.columns | object[] | |
report.columns[].align | string | One of: left, right |
report.columns[].field required | string | One of: event, notification_id, payment_id, request_id, mode, biller, biller_name, product, supplier_product, reference, invoice_number, amount, payer_fee, total, currency, amount_minor, total_minor, paid_at, channel_id, channel_name, funding, partner_reference, supplier_reference, receipt_url |
report.columns[].header | string | |
report.columns[].width | integer | txt (fixed width) only. |
report.date_format | string | |
report.decimal_separator | string | One of: ., , |
report.enabled | boolean | |
report.file_name | string | Placeholders {biller}, {product}, {date}, {mode}; the extension is the one given, else the format's. |
report.format | string | One of: csv, tsv, psv, txt, json, xml, xlsx |
report.header | boolean | |
report.line_ending | string | One of: lf, crlf |
report.url | string (uri) |
Responses
- 200 The integration.
- 400
invalid_request. - 404 No such biller or product.
not_found
Request
curl -X PUT "$KULPAY_URL/v1/biller/products/{product}/integration" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"notification": {
"auth": {
"secret": "<o seu segredo>",
"type": "hmac"
},
"content_type": "application/xml",
"enabled": true,
"headers": {
"X-Biller-Code": "AGUAS-01"
},
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
"url": "https://lsp.example/kulpay/pagamentos"
},
"report": {
"auth": {
"token": "<o seu token>",
"type": "bearer"
},
"columns": [
{
"field": "paid_at",
"header": "DATA",
"width": 10
},
{
"field": "invoice_number",
"header": "FACTURA",
"width": 12
},
{
"align": "right",
"field": "total",
"header": "VALOR",
"width": 10
}
],
"date_format": "DD/MM/YYYY",
"decimal_separator": ",",
"enabled": true,
"file_name": "AGUAS_{date}.dat",
"format": "txt",
"url": "https://lsp.example/kulpay/reconciliacao"
}
}'
200 Response application/json
{
"biller": "AGUAS",
"fields": [
"event",
"notification_id",
"payment_id",
"request_id",
"mode",
"biller",
"biller_name",
"product",
"supplier_product",
"reference",
"invoice_number",
"amount",
"payer_fee",
"total",
"currency",
"amount_minor",
"total_minor",
"paid_at",
"channel_id",
"channel_name",
"funding",
"partner_reference",
"supplier_reference",
"receipt_url"
],
"mode": "test",
"notification": {
"auth": "hmac",
"content_type": "application/xml",
"enabled": true,
"headers": {
"X-Biller-Code": "AGUAS-01"
},
"method": "POST",
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
"url": "https://lsp.example/kulpay/pagamentos"
},
"product": "FACTURA",
"report": {
"auth": "bearer",
"columns": [
{
"field": "paid_at",
"header": "DATA",
"width": 10
},
{
"field": "invoice_number",
"header": "FACTURA",
"width": 12
},
{
"align": "right",
"field": "total",
"header": "VALOR",
"width": 10
}
],
"date_format": "DD/MM/YYYY",
"decimal_separator": ",",
"enabled": true,
"file_name": "AGUAS_{date}.dat",
"format": "txt",
"header": true,
"last_day": "2026-10-19",
"line_ending": "lf",
"url": "https://lsp.example/kulpay/reconciliacao"
},
"updated_at": "2026-10-20T08:00:00Z",
"version": 3
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "notification: the template does not render well-formed XML (use {{xml .field}} for values)",
"error": {
"code": "invalid_request",
"message": "notification: the template does not render well-formed XML (use {{xml .field}} for values)"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/products/{product}/integration/deliveries
A product's notifications and reports sent (biller)
The deliveries to the LSP, newest first: status pending (retrying), delivered or exhausted, attempts, the endpoint's last answer. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
kind | query | string | One of: notification, report |
Responses
- 200 The deliveries.
- 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/products/{product}/integration/deliveries" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"attempts": 1,
"content_type": "application/xml",
"created_at": "2026-10-20T08:00:00Z",
"delivered_at": "2026-10-20T08:00:01Z",
"duration_ms": 84,
"id": "3KvxDelivery00000000000001",
"integration_id": "3KvxIntegration0000000001",
"kind": "notification",
"method": "POST",
"mode": "test",
"next_attempt_at": "2026-10-20T08:00:00Z",
"response_code": 200,
"status": "delivered",
"subject_id": "3KvxPayment000000000000001",
"updated_at": "2026-10-20T08:00:01Z",
"url": "https://lsp.example/kulpay/pagamentos"
}
]
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/products/{product}/integration/deliveries/{id}
One delivery and its exact body (biller)
What was sent, byte for byte: body (text) or body_base64 (xlsx). Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
id required | path | string | The delivery id. |
Responses
- 200 The delivery.
- 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/products/{product}/integration/deliveries/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"body": "<pagamento><referencia>CL-1</referencia><valor>151000</valor><data>19/10/2026 12:00</data></pagamento>",
"delivery": {
"attempts": 1,
"content_type": "application/xml",
"created_at": "2026-10-20T08:00:00Z",
"delivered_at": "2026-10-20T08:00:01Z",
"duration_ms": 84,
"id": "3KvxDelivery00000000000001",
"integration_id": "3KvxIntegration0000000001",
"kind": "notification",
"method": "POST",
"mode": "test",
"next_attempt_at": "2026-10-20T08:00:00Z",
"response_code": 200,
"status": "delivered",
"subject_id": "3KvxPayment000000000000001",
"updated_at": "2026-10-20T08:00:01Z",
"url": "https://lsp.example/kulpay/pagamentos"
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/biller/products/{product}/integration/deliveries/{id}/retry
Send a delivery again (biller)
Sends a pending or exhausted delivery again now, the same body. Requires webhooks:manage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
id required | path | string | The delivery id. |
Responses
- 200 The delivery after the attempt.
- 404 No such biller or product.
not_found
Request
curl -X POST "$KULPAY_URL/v1/biller/products/{product}/integration/deliveries/{id}/retry" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"body": "<pagamento>…</pagamento>",
"delivery": {
"attempts": 1,
"content_type": "application/xml",
"created_at": "2026-10-20T08:00:00Z",
"delivered_at": "2026-10-20T08:00:01Z",
"duration_ms": 84,
"id": "3KvxDelivery00000000000001",
"integration_id": "3KvxIntegration0000000001",
"kind": "notification",
"method": "POST",
"mode": "test",
"next_attempt_at": "2026-10-20T08:00:00Z",
"response_code": 200,
"status": "delivered",
"subject_id": "3KvxPayment000000000000001",
"updated_at": "2026-10-20T08:00:01Z",
"url": "https://lsp.example/kulpay/pagamentos"
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/biller/products/{product}/integration/preview
Preview a notification's body (biller)
Renders a template (the one given, else the product's) for a sample payment, as the LSP would receive it. Requires webhooks:manage.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
Request body application/json required
| Field | Type | Description |
|---|---|---|
content_type | string | |
template | string |
Responses
- 200 The rendered body.
- 400
invalid_request. - 404 No such biller or product.
not_found
Request
curl -X POST "$KULPAY_URL/v1/biller/products/{product}/integration/preview" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"content_type": "application/xml",
"template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>"
}'
200 Response application/json
{
"body": "<pagamento><referencia>EX-123456</referencia><valor>126000</valor><data>19/10/2026 16:30</data></pagamento>",
"content_type": "application/xml"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "template: unexpected \"}\" in operand",
"error": {
"code": "invalid_request",
"message": "template: unexpected \"}\" in operand"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/products/{product}/reports/{day}
A product's report for a day (biller)
The product's payments that succeeded on the day (Maputo), in its report format and file name (Content-Disposition). Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
product required | path | string | The product code (KulPay's). |
day required | path | string (date) | The day, YYYY-MM-DD, Maputo time. |
Responses
- 200 The file, in the product's format.
- 400
invalid_request. - 404 No such biller or product.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/products/{product}/reports/{day}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response text/plain
DATA FACTURA VALOR 19/10/2026F-N1 1510,00
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the day is YYYY-MM-DD",
"error": {
"code": "invalid_request",
"message": "the day is YYYY-MM-DD"
},
"status": 400,
"title": "Invalid request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/biller/remittances
Your remittances
What KulPay remitted, or will remit, to you. Requires invoices:read.
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/biller/remittances" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"count": 1,
"created_at": "2026-10-10T22:14:59.538329669Z",
"currency": "MZN",
"fee_minor": 0,
"gross_minor": 150000,
"id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
"mode": "test",
"net_minor": 150000,
"period_from": "2026-10-10T22:14:59.495269929Z",
"period_to": "2026-10-10T22:14:59.495269929Z",
"status": "pending"
}
],
"total": 1
}
GET /v1/biller/remittances/{id}/report.csv
Your remittance report
As the operators see it, for reconciling your customers' accounts. Requires invoices:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string |
Responses
- 200 CSV.
- 404 Not yours.
not_found
Request
curl -X GET "$KULPAY_URL/v1/biller/remittances/{id}/report.csv" \
-H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/billers
Billers and products you may sell
The active billers with the enabled products open to your account (BIL-1.3): amount rules, the customer reference to ask for, whether a lookup is required, the fee the payer pays, and offers (bundles, bouquets). Filter by category and q (name or code). Responses carry an ETag and Cache-Control: max-age=300; send If-None-Match to revalidate. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
category | query | string | One of: electricity, water, telecom, tv, internet, education, insurance, government, other |
q | query | string |
Responses
- 200
{data, total}. - 304 Not modified.
Request
curl -X GET "$KULPAY_URL/v1/billers" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"category": "tv",
"code": "TVCABO",
"country": "MZ",
"name": "TV Cabo",
"products": [
{
"amount_rule": "fixed",
"code": "PACOTE",
"currency": "MZN",
"customer_reference": {
"label": "Smartcard",
"pattern": "^\\d{10}$"
},
"fee": {
"bearer": "payer",
"currency": "MZN",
"fixed": "5.00"
},
"fixed": "750.00",
"lookup_required": false,
"name": "Pacote",
"offers": [
{
"code": "TVBASIC",
"currency": "MZN",
"name": "Pacote Básico",
"price": "750.00",
"price_minor": 75000,
"validity": "30d"
},
{
"code": "TVPREMIUM",
"currency": "MZN",
"name": "Pacote Premium",
"price": "2500.00",
"price_minor": 250000,
"validity": "30d"
}
],
"shape": "account_topup"
}
],
"support_phone": "+258 21 480 500"
}
],
"total": 1
}
GET /v1/billers/{code}
One biller you may sell
As in the listing. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
Responses
- 200 The biller.
- 304 Not modified.
- 404 No such biller, product or supply connector.
not_found
Request
curl -X GET "$KULPAY_URL/v1/billers/{code}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"category": "tv",
"code": "TVCABO",
"country": "MZ",
"name": "TV Cabo",
"products": [
{
"amount_rule": "fixed",
"code": "PACOTE",
"currency": "MZN",
"customer_reference": {
"label": "Smartcard",
"pattern": "^\\d{10}$"
},
"fee": {
"bearer": "payer",
"currency": "MZN",
"fixed": "5.00"
},
"fixed": "750.00",
"lookup_required": false,
"name": "Pacote",
"offers": [
{
"code": "TVBASIC",
"currency": "MZN",
"name": "Pacote Básico",
"price": "750.00",
"price_minor": 75000,
"validity": "30d"
},
{
"code": "TVPREMIUM",
"currency": "MZN",
"name": "Pacote Premium",
"price": "2500.00",
"price_minor": 250000,
"validity": "30d"
}
],
"shape": "account_topup"
}
],
"support_phone": "+258 21 480 500"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/billers/{code}/products/{product}/offers
A product's offers
The active offers of one product you may sell (IG-35), cheapest first, each with its decimal price, the exact price_minor and the product's currency, and the product's purchase_modes. Inactive offers are not listed. Responses carry an ETag and Cache-Control: max-age=300; send If-None-Match to revalidate. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
code required | path | string | The biller code, e.g. `EDM`. |
product required | path | string | The product code, e.g. `CREDELEC`. |
Responses
- 200
{data, purchase_modes}. - 304 Not modified.
- 404 No such biller, product or supply connector.
not_found
Request
curl -X GET "$KULPAY_URL/v1/billers/{code}/products/{product}/offers" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"code": "TVBASIC",
"currency": "MZN",
"name": "Pacote Básico",
"price": "750.00",
"price_minor": 75000,
"validity": "30d"
},
{
"code": "TVPREMIUM",
"currency": "MZN",
"name": "Pacote Premium",
"price": "2500.00",
"price_minor": 250000,
"validity": "30d"
}
],
"purchase_modes": [
"fixed_catalog",
"variable_amount"
]
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such biller, product or supply connector",
"error": {
"code": "not_found",
"message": "no such biller, product or supply connector"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/payers
Register or update a payer
Your payer in KulPay's payer directory (BIL-5.4), under your own external_id: 201 created, 200 updated. Name, phone and email are sealed and never put in events; phone and email are matched, as keyed hashes, to billers' enrolments, which subscribe the payer (bill_subscription.opted_in with origin: biller_enrolled), or, with enrolments: offer, offer the biller account to them (bill_subscription.offered, status offered): nothing of it is delivered until the payer accepts by subscribing with their consent, or declines by opting out. enrolments is kept when left out. Requires bills:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
email | string (email) | |
enrolments | string | What a biller's matching enrolment does: subscribe (default; the payer can opt out) or offer (the payer is asked first). One of: subscribe, offer |
external_id required | string | |
name | string | |
phone | string |
Responses
- 200 The payer.
- 201 The payer.
- 400
invalid_request.
Request
curl -X POST "$KULPAY_URL/v1/payers" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "u-1",
"name": "Ana",
"phone": "+258841234567"
}'
200 Response application/json
{
"created_at": "2026-06-14T08:30:00Z",
"email": "ana.macuacuaexample.co.mz",
"external_id": "CLI-000482",
"id": "3K4mvOpRKqOU8KwHnmvdbsC0spb",
"name": "Ana Macuácua",
"phone": "258841234567"
}
201 Response application/json
{
"created_at": "2026-10-10T22:14:59.658576008Z",
"external_id": "u-1",
"id": "3KWPlDfQKeSRExAGpYcLFhGY2jR",
"name": "Ana",
"phone": "258841234567"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "email is not an email address",
"error": {
"code": "invalid_request",
"message": "email is not an email address"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/payers/{id}
A payer
Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
Responses
- 200 The payer.
- 404 Not yours or unknown.
not_found
Request
curl -X GET "$KULPAY_URL/v1/payers/{id}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"created_at": "2026-08-14T12:03:55Z",
"email": "joana.cossaexample.co.mz",
"external_id": "cust-000184",
"id": "3Kpx6EPL2FOQLXxXg0PeZMIUgIx",
"name": "Joana Cossa",
"phone": "+258841234567"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/payers/{id}/invoices
A payer's open bills
The open invoices of the billers the payer is subscribed to (source: presented, pay with invoice_id), and the amount due at lookup billers, asked now (source: lookup, pay with the lookup_id and product; unavailable when the biller does not answer) (BIL-5.6). New invoices arrive as bill_invoice.delivered webhooks, and bill_invoice.reminded three days before they are due. Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
Responses
- 200
{data, total}. - 404 Not yours or unknown.
not_found
Request
curl -X GET "$KULPAY_URL/v1/payers/{id}/invoices" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"account_reference": "CL-9",
"alias": "Casa",
"biller": "AGUAS",
"biller_name": "Águas",
"invoice": {
"account_reference": "CL-9",
"amount": "300.00",
"biller": "AGUAS",
"biller_name": "Águas",
"currency": "MZN",
"due_date": "2026-12-31",
"expires_at": "2027-01-30T00:00:00Z",
"id": "3KWPlFgLlzNeKjlNMg1Xp9qGbkU",
"issue_date": "2026-10-10",
"number": "F-9",
"open": "300.00",
"paid": "0.00",
"status": "open"
},
"source": "presented"
}
],
"total": 1
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/payers/{id}/subscriptions
A payer's subscriptions
Every status: active, pending_verification, opted_out, ended (the biller removed the enrolment). Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
Responses
- 200
{data, total}.
Request
curl -X GET "$KULPAY_URL/v1/payers/{id}/subscriptions" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"account_reference": "CL-9",
"alias": "Casa",
"biller": "AGUAS",
"biller_name": "Águas",
"consent_at": "2026-10-10T22:14:59.682786547Z",
"consent_version": "2026-10",
"id": "3KWPlDuqLcP8vv2BFC9FcZqeDgd",
"opted_out_at": "2026-10-10T22:14:59.73153362Z",
"origin": "payer",
"status": "opted_out"
}
],
"total": 1
}
POST /v1/payers/{id}/subscriptions
Opt a payer in to a biller's bills
The payer chooses to receive a biller account's bills through your channel (BIL-5.4). consent is required: accepted: true and the text_version of the terms they accepted (422 consent_required); KulPay records it with the time and your channel. An account the biller can look up is verified first (422 reference_not_found; pending_verification while the biller is unreachable), and the customer's name is returned masked. An account of a biller that pushes or files its invoices is active only when the biller's enrolment of that account names the payer's phone or email; otherwise it is pending_verification, with nothing delivered or listed, until such an enrolment arrives. Opting in again after opting out reactivates the subscription, and subscribing to an offered one accepts it. Records bill_subscription.opted_in. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
account_reference required | string | |
alias | string | |
biller required | string | |
consent required | object | |
consent.accepted required | boolean | |
consent.text_version required | string |
Responses
- 201 The subscription (
activeorpending_verification). - 404 Unknown payer or biller.
not_found - 422
consent_required,reference_not_found.
Request
curl -X POST "$KULPAY_URL/v1/payers/{id}/subscriptions" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"account_reference": "CL-9",
"alias": "Casa",
"biller": "AGUAS",
"consent": {
"accepted": true,
"text_version": "2026-10"
}
}'
201 Response application/json
{
"account_reference": "CL-9",
"alias": "Casa",
"biller": "AGUAS",
"biller_name": "Águas",
"consent_at": "2026-10-10T22:14:59.682786547Z",
"consent_version": "2026-10",
"id": "3KWPlDuqLcP8vv2BFC9FcZqeDgd",
"origin": "payer",
"status": "active"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · consent_required application/problem+json
{
"code": "consent_required",
"detail": "the payer's consent is required: consent.accepted and the version of the terms they accepted",
"error": {
"code": "consent_required",
"message": "the payer's consent is required: consent.accepted and the version of the terms they accepted"
},
"status": 422,
"title": "Unprocessable Content",
"type": "about:blank"
}
DELETE /v1/payers/{id}/subscriptions/{sid}
Opt a payer out
Withdraws the subscription at once (BIL-5.4), or declines an offered one: nothing of that biller account is delivered any more, and a later enrolment by the biller does not subscribe (nor offer) it to the payer again. Optional ?reason=. Records bill_subscription.opted_out. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
sid required | path | string | |
reason | query | string |
Responses
- 200 The subscription,
opted_out. - 404 Not this payer's.
not_found
Request
curl -X DELETE "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"account_reference": "CL-9",
"alias": "Casa",
"biller": "AGUAS",
"biller_name": "Águas",
"consent_at": "2026-10-10T22:14:59.682786547Z",
"consent_version": "2026-10",
"id": "3KWPlDuqLcP8vv2BFC9FcZqeDgd",
"opted_out_at": "2026-10-10T22:14:59.73153362Z",
"origin": "payer",
"status": "opted_out"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
GET /v1/payers/{id}/subscriptions/{sid}/autopay
A payer's autopay
The subscription's mandate and its recent invoices (BIL-5.10). Requires bills:read.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
sid required | path | string | The subscription id. |
Responses
- 200 The mandate.
- 404 No such payer, subscription or mandate.
not_found
Request
curl -X GET "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"consent_at": "2026-10-19T09:30:00Z",
"consent_version": "autopay-terms-v1",
"currency": "MZN",
"days_before_due": 3,
"funding": "collect",
"id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
"max_amount": "2000.00",
"method": "mpesa",
"mode": "live",
"phone_masked": "25884****000",
"runs": [
{
"amount": "1500.00",
"attempts": 0,
"currency": "MZN",
"id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
"invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
"invoice_number": "FT-2026-0412",
"next_attempt_at": "2026-11-07T00:00:00Z",
"scheduled_for": "2026-11-07T00:00:00Z",
"status": "scheduled"
}
],
"status": "active",
"subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
PUT /v1/payers/{id}/subscriptions/{sid}/autopay
Set up a payer's autopay
Gives (or changes) the payer's mandate to pay the subscription's invoices automatically (BIL-5.10): funding balance (your balance; debit the payer's own account when told bill_autopay.succeeded) or collect (their wallet: collect.method mpesa, emola or mkesh, collect.phone, the payer's phone when left out); max_amount, the most one invoice is debited for; days_before_due (0 to 15); and the payer's consent to the autopay terms. Records bill_autopay.mandate_set. Each invoice delivered to the subscription, and each one open now, is scheduled for the later of its due date less days_before_due and now plus the pre-debit notice (bills.autopay_notice_hours, at least 24 hours in live mode), and you are told at once with bill_autopay.scheduled: notify the payer before the debit. An invoice above max_amount is not debited (bill_autopay.skipped, over_limit). A failed debit is tried again (bill_autopay.attempt_failed) up to bills.autopay_attempts times, then given up (bill_autopay.failed). The mandate runs in your key's mode. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
sid required | path | string | The subscription id. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
collect | object | |
collect.method | string | One of: mpesa, emola, mkesh |
collect.network | string | |
collect.phone | string | |
consent required | object | |
consent.accepted required | boolean | |
consent.text_version required | string | |
days_before_due | integer | |
funding | string | One of: balance, collect |
max_amount required | string |
Responses
- 200 The mandate,
active, with its scheduled invoices. - 400 Invalid funding, wallet, max_amount or days_before_due, or the subscription is not active.
invalid_request - 404 No such payer or subscription.
not_found - 422
consent_required: the payer's consent to the autopay terms.
Request
curl -X PUT "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"collect": {
"method": "mpesa",
"phone": "258841110000"
},
"consent": {
"accepted": true,
"text_version": "autopay-terms-v1"
},
"days_before_due": 3,
"funding": "collect",
"max_amount": "2000.00"
}'
200 Response application/json
{
"consent_at": "2026-10-19T09:30:00Z",
"consent_version": "autopay-terms-v1",
"currency": "MZN",
"days_before_due": 3,
"funding": "collect",
"id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
"max_amount": "2000.00",
"method": "mpesa",
"mode": "live",
"phone_masked": "25884****000",
"runs": [
{
"amount": "1500.00",
"attempts": 0,
"currency": "MZN",
"id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
"invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
"invoice_number": "FT-2026-0412",
"next_attempt_at": "2026-11-07T00:00:00Z",
"scheduled_for": "2026-11-07T00:00:00Z",
"status": "scheduled"
}
],
"status": "active",
"subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "max_amount, the most one invoice may be debited for, is a positive decimal",
"error": {
"code": "invalid_request",
"message": "max_amount, the most one invoice may be debited for, is a positive decimal"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
422 Error · consent_required application/problem+json
{
"code": "consent_required",
"detail": "the payer's consent to autopay is required: consent.accepted and the version of the terms they accepted",
"error": {
"code": "consent_required",
"message": "the payer's consent to autopay is required: consent.accepted and the version of the terms they accepted"
},
"status": 422,
"title": "Unprocessable Entity",
"type": "about:blank"
}
DELETE /v1/payers/{id}/subscriptions/{sid}/autopay
Cancel a payer's autopay
Cancels the mandate at once (BIL-5.10): its scheduled invoices are not debited (bill_autopay.skipped, mandate_cancelled). Optional ?reason=. Records bill_autopay.mandate_cancelled; opting the payer out of the subscription cancels it too. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
sid required | path | string | The subscription id. |
reason | query | string |
Responses
- 200 The mandate,
cancelled. - 404 No such payer, subscription or mandate.
not_found
Request
curl -X DELETE "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"cancel_reason": "payer asked",
"cancelled_at": "2026-10-20T08:00:00Z",
"consent_at": "2026-10-19T09:30:00Z",
"consent_version": "autopay-terms-v1",
"currency": "MZN",
"days_before_due": 3,
"funding": "collect",
"id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
"max_amount": "2000.00",
"method": "mpesa",
"mode": "live",
"phone_masked": "25884****000",
"runs": [
{
"amount": "1500.00",
"attempts": 0,
"currency": "MZN",
"id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
"invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
"invoice_number": "FT-2026-0412",
"reason": "mandate_cancelled",
"scheduled_for": "2026-11-07T00:00:00Z",
"status": "skipped"
}
],
"status": "cancelled",
"subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/payers/{id}/subscriptions/{sid}/autopay/skip
Skip one invoice's autopay
The invoice is not debited (BIL-5.10): bill_autopay.skipped, payer_skipped. Only a scheduled invoice can be skipped. Requires bills:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | The KulPay payer id or your external_id. |
sid required | path | string | The subscription id. |
Request body application/json required
| Field | Type | Description |
|---|---|---|
invoice_id required | string |
Responses
- 200 The mandate.
- 400 The invoice is no longer scheduled.
invalid_request - 404 No such payer, subscription, mandate or scheduled invoice.
not_found
Request
curl -X POST "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay/skip" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8"
}'
200 Response application/json
{
"consent_at": "2026-10-19T09:30:00Z",
"consent_version": "autopay-terms-v1",
"currency": "MZN",
"days_before_due": 3,
"funding": "collect",
"id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
"max_amount": "2000.00",
"method": "mpesa",
"mode": "live",
"phone_masked": "25884****000",
"runs": [
{
"amount": "1500.00",
"attempts": 0,
"currency": "MZN",
"id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
"invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
"invoice_number": "FT-2026-0412",
"reason": "payer_skipped",
"scheduled_for": "2026-11-07T00:00:00Z",
"status": "skipped"
}
],
"status": "active",
"subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "the invoice's autopay is paid and can no longer be skipped",
"error": {
"code": "invalid_request",
"message": "the invoice's autopay is paid and can no longer be skipped"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such payer, subscription or enrolment",
"error": {
"code": "not_found",
"message": "no such payer, subscription or enrolment"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}