KulPayAPI Live

API reference

Bill payments

Billers, products and supply connectors (epics 14–19): prepaid electricity, top-ups, airtime and data, postpaid bills and vouchers.

POST /sms/inbound

The SMS provider forwards payers' replies

For the SMS provider, not for partners (F3-15): a payer's reply to an invoice notice, {from, text} as JSON or a form, authenticated by the shared token configured as KULPAY_BILLS_SMS_INBOUND_TOKEN (Authorization: Bearer). STOP (or PARAR, PARE, SAIR, CANCELAR) stops the notices of every enrolment with that number at once and withdraws those still waiting; any other reply is ignored. Without a configured token every call is refused.

Request body application/json required

FieldTypeDescription
from requiredstring
text requiredstring

Responses

  • 200 {stopped}: how many enrolments stopped.
  • 401 The token is missing or wrong. unauthorized

Request

curl -X POST "$KULPAY_URL/sms/inbound" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "from": "+258841234567",
  "text": "STOP"
}'
200 Response application/json
{
  "stopped": 1
}
401 Error · unauthorized application/problem+json
{
  "code": "unauthorized",
  "detail": "The SMS provider's token is missing or wrong.",
  "error": {
    "code": "unauthorized",
    "message": "The SMS provider's token is missing or wrong."
  },
  "status": 401,
  "title": "Unauthorized",
  "type": "about:blank"
}

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

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
currency requiredstring
low_balance_minor requiredinteger

Responses

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-channels/{id}/access

A channel's access to full names

A bill lookup answers the customer's name masked. A channel whose contract allows it may see the full name (F3-17): an operator switches it on with the contract's reference and a second approves (bill.channel_unmask, bill_channel.unmasking_changed). Every lookup that reveals a name is recorded in the audit trail as bill_lookup.name_revealed, the name masked there. Switching it off takes effect once approved. Requires admin:read.

Parameters

NameInTypeDescription
id requiredpathstring The channel (organisation) id.

Responses

  • 200 The access; the default (masked) when never set.
  • 404 No such channel. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/bill-channels/{id}/access" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "approval_id": "3Kb2fIkT33EXgUXHykp4aEZ0CXf",
  "channel_id": "3Kb2fN21UgrE7TzaEzY5HSocv5M",
  "contract_reference": "CTR-2026-014",
  "unmasked_names": true,
  "updated_at": "2026-10-12T13:34:09.017827097Z"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such channel",
  "error": {
    "code": "not_found",
    "message": "no such channel"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/admin/bill-channels/{id}/access

Let a channel see full names, or stop it

A bill lookup answers the customer's name masked. A channel whose contract allows it may see the full name (F3-17): an operator switches it on with the contract's reference and a second approves (bill.channel_unmask, bill_channel.unmasking_changed). Every lookup that reveals a name is recorded in the audit trail as bill_lookup.name_revealed, the name masked there. Switching it off takes effect once approved. Requires admin:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
contract_referencestring Required to switch it on.
reason requiredstring
unmasked_names requiredboolean

Responses

  • 202 {approval}.
  • 400 unmasked_names missing, or switched on without a contract_reference. invalid_request
  • 404 No such channel. not_found

Request

curl -X PUT "$KULPAY_URL/v1/admin/bill-channels/{id}/access" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "contract_reference": "CTR-2026-014",
  "reason": "Contract clause 7.2 allows full names",
  "unmasked_names": true
}'
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": "unmasked_names is required",
  "error": {
    "code": "invalid_request",
    "message": "unmasked_names is required"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such channel",
  "error": {
    "code": "not_found",
    "message": "no such channel"
  },
  "status": 404,
  "title": "Not Found",
  "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

NameInTypeDescription
channel requiredquerystring
monthquerystring

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

NameInTypeDescription
statusquerystring
billerquerystring
channelquerystring
customer_referencequerystring
limitqueryinteger

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

NameInTypeDescription
id requiredpathstring

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.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

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"
}

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

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
outcome requiredstring
One of: succeeded, failed
reason requiredstring
supplier_referencestring
tokenstring
unitsstring

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"
}

POST /v1/admin/bill-payments/{id}/reverse

Reverse a succeeded bill payment

Reverses a succeeded agent-model payment in full (F3-16): its money did not reach the biller after all, the channel refunded its customer, or the biller reports it never received it. One operator asks with a reason; a second approves (bill.reverse). The reversal undoes the ledger (the biller's payable, KulPay's fee and the channel's commission) and gives the total back to the channel's balance; a payment collected from the payer first is refunded to the payer (bill_payment.refunded, status reversed). The channel receives bill_payment.reversed. Its invoice opens again (paid to open, or partially_paid while other payments remain) and the biller receives bill_invoice.reopened with the reason; when an earlier remittance already paid the biller that payment, the next remittance deducts it (adjustment_minor). Principal-model sales (prepaid tokens, vouchers) are final with their supplier and are refused. Requires admin:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

  • 202 {approval}.
  • 400 No reason, not succeeded, or a principal-model sale. invalid_request
  • 404 No such bill payment. not_found

Request

curl -X POST "$KULPAY_URL/v1/admin/bill-payments/{id}/reverse" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "The biller'\''s bank returned the credit"
}'
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": "a reason of at most 500 characters is required",
  "error": {
    "code": "invalid_request",
    "message": "a reason of at most 500 characters 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"
}

GET /v1/admin/bill-recon

Supplier reconciliation runs

Recent runs with their breaks, of a connector or all. Requires reconciliation:read.

Parameters

NameInTypeDescription
connectorquerystring

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

NameInTypeDescription
statusquerystring
One of: draft, active, suspended, retired
categoryquerystring
qquerystring

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": "apoio@example.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": "clientes@example.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

FieldTypeDescription
categorystring
One of: electricity, water, telecom, tv, internet, education, insurance, government, other
codestring
countrystring
logo_urlstring (uri)
namestring
settlement_modestring Remitted per payment (immediate, within the minute) or in the daily batch (batch, the default), by the rail of its credit account (IO-4.3).
One of: immediate, batch
support_emailstring (email)
support_phonestring
tenant_idstring An onboarded organisation with the biller role, if any.

Responses

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

NameInTypeDescription
code requiredpathstring 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; remit_bank and remit_account are set here only while the biller is a draft, then with PUT /v1/admin/billers/{code}/remit-account (four eyes); the status changes only through /status. Records biller.updated. Requires connectors:write.

Parameters

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.

Request body application/json required

FieldTypeDescription
categorystring
One of: electricity, water, telecom, tv, internet, education, insurance, government, other
codestring
countrystring
logo_urlstring (uri)
namestring
settlement_modestring Remitted per payment (immediate, within the minute) or in the daily batch (batch, the default), by the rail of its credit account (IO-4.3).
One of: immediate, batch
support_emailstring (email)
support_phonestring
tenant_idstring An onboarded organisation with the biller role, if any.

Responses

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"
}

POST /v1/admin/billers/{code}/certification

Certify a KulOffice biller's integration

For a biller onboarded in KulOffice (IO-3.5), in the sandbox: checks its gates (product_ready; sandbox_payment_succeeded, a test-mode payment of the biller that succeeded through its contract; invoice_presented when it presents invoices). All met, its technical status becomes certified (biller.updated, and biller.integration_status to KulOffice); it then goes live with POST /v1/admin/billers/{code}/status and a second operator. Calling it again is safe. Requires connectors:write.

Parameters

NameInTypeDescription
code requiredpathstring The biller code, e.g. `AGUAS_DE_MAPUTO`.

Responses

  • 200 The certification: its gates and whether it passed.
  • 400 invalid_request: a KulPay biller, or one not in the sandbox.
  • 404 No such biller, product or supply connector. not_found

Request

curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/certification" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "biller_id": "bil_123",
  "certified": true,
  "code": "AGUAS_DE_MAPUTO",
  "gates": [
    {
      "met": true,
      "name": "product_ready"
    },
    {
      "met": true,
      "name": "sandbox_payment_succeeded"
    },
    {
      "met": true,
      "name": "invoice_presented"
    }
  ],
  "technical_status": "certified"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "biller EDM is KulPay's own: it goes live by its activation alone",
  "error": {
    "code": "invalid_request",
    "message": "biller EDM is KulPay's own: it goes live by its activation alone"
  },
  "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

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

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.

Request body application/json required

FieldTypeDescription
amount_rulestring
One of: range, fixed, denominations, presented
codestring
connectorsstring[] Supply connector names, in priority order.
currencystring
denominationsinteger[]
discount_bpsinteger The supplier's discount to KulPay in basis points (principal model, biller bears the fee).
enabledboolean
fee_bearerstring
One of: payer, biller
fee_bpsinteger Percentage fee in basis points (150 = 1.50%).
fee_fixed_minorinteger Fixed fee.
fee_max_minorinteger Maximum fee.
fee_min_minorinteger Minimum fee.
fixed_minorinteger Fixed amount.
lookup_requiredboolean
max_minorinteger Range maximum.
min_minorinteger Range minimum.
modelstring
One of: principal, agent
namestring
reference_examplestring
reference_labelstring
reference_patternstring A regular expression the reference must match.
shapestring
One of: metered_prepaid, account_topup, airtime, postpaid, voucher
supplier_productstring The product's code at the supplier.

Responses

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

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.
product requiredpathstring The product code, e.g. `CREDELEC`.

Request body application/json required

FieldTypeDescription
amount_rulestring
One of: range, fixed, denominations, presented
codestring
connectorsstring[] Supply connector names, in priority order.
currencystring
denominationsinteger[]
discount_bpsinteger The supplier's discount to KulPay in basis points (principal model, biller bears the fee).
enabledboolean
fee_bearerstring
One of: payer, biller
fee_bpsinteger Percentage fee in basis points (150 = 1.50%).
fee_fixed_minorinteger Fixed fee.
fee_max_minorinteger Maximum fee.
fee_min_minorinteger Minimum fee.
fixed_minorinteger Fixed amount.
lookup_requiredboolean
max_minorinteger Range maximum.
min_minorinteger Range minimum.
modelstring
One of: principal, agent
namestring
reference_examplestring
reference_labelstring
reference_patternstring A regular expression the reference must match.
shapestring
One of: metered_prepaid, account_topup, airtime, postpaid, voucher
supplier_productstring The product's code at the supplier.

Responses

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

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.
product requiredpathstring The product code, e.g. `CREDELEC`.

Request body application/json required

FieldTypeDescription
channelsstring[]

Responses

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"
}

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

NameInTypeDescription
code requiredpathstring
product requiredpathstring

Request body application/json required

FieldTypeDescription
channel_daily_minorinteger
reason requiredstring
reference_daily_countinteger
reference_daily_minorinteger

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

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.
product requiredpathstring The product code, e.g. `CREDELEC`.

Request body application/json required

FieldTypeDescription
activeboolean
code requiredstring
name requiredstring
price_minor requiredinteger Price.
validitystring

Responses

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

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.
product requiredpathstring The product code, e.g. `CREDELEC`.

Responses

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"
}

PUT /v1/admin/billers/{code}/remit-account

Change a biller's settlement account (maker)

Where a biller is paid is a fraud target: a change waits for a second person (POST /v1/admin/approvals/{id}/decide). account is the NIB (21 digits, check digits verified; spaces ignored), holder the account holder as on the bank's records, bank optional (the NIB's bank code by default). Approved, the account is set, biller.updated recorded, and the biller's support email and KulPay's compliance contacts are told by email. Past draft, PATCH /v1/admin/billers/{code} refuses remit_bank and remit_account. Requires reconciliation:write.

Parameters

NameInTypeDescription
code requiredpathstring

Request body application/json required

FieldTypeDescription
account requiredstring The NIB.
bankstring
holder requiredstring
reason requiredstring

Responses

Request

curl -X PUT "$KULPAY_URL/v1/admin/billers/{code}/remit-account" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "account": "000300010000001234585",
  "bank": "BCI",
  "holder": "Águas da Região de Maputo, SA",
  "reason": "the biller'\''s bank letter"
}'
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:14:59.570339309Z",
    "id": "3KWPlDhVsFz6D7Nlw0sljeII8l1",
    "kind": "biller.remit_account",
    "payload": "{\"bank\":\"BCI\",\"account\":\"000300010000001234585\",\"holder\":\"Águas da Região de Maputo, SA\"}",
    "prepared_by": "key_accounts",
    "reason": "settlement account of AGUAS to *****************4585 (Águas da Região de Maputo, SA): the biller's bank letter",
    "status": "pending",
    "subject_id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
    "tenant_id": "platform"
  },
  "message": "change requested; a second person must approve it (POST /v1/admin/approvals/{id}/decide)"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "account is the biller's NIB: 21 digits whose check digits verify",
  "error": {
    "code": "invalid_request",
    "message": "account is the biller's NIB: 21 digits whose check digits verify"
  },
  "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}/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

NameInTypeDescription
code requiredpathstring

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

NameInTypeDescription
code requiredpathstring The biller code, e.g. `EDM`.

Request body application/json required

FieldTypeDescription
reason requiredstring
status requiredstring
One of: active, suspended, retired

Responses

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

NameInTypeDescription
channelquerystring

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

FieldTypeDescription
billerstring
bpsinteger
channel_idstring
direction requiredstring
One of: we_pay, we_charge
effective_fromstring (date)
fixed_minorinteger
productstring
reason requiredstring
tiersobject[]
tiers[].bpsinteger
tiers[].from_minorinteger

Responses

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

NameInTypeDescription
connectorquerystring

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/remittances

Remittances to billers

Newest first, of one biller or all. Requires reconciliation:read.

Parameters

NameInTypeDescription
billerquerystring

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 paid by hand

For a bank without files: the transfer treasury made by hand, its reference and a reason. A second person approves; the remittance is marked paid_by: manual, the biller payable is cleared against client funds and the biller receives bill_remittance.paid. A remittance whose transfer is in the bank rail (paying) cannot be recorded by hand. Prefer POST /v1/admin/remittances/{id}/pay. Requires reconciliation:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
bank_reference requiredstring
reason requiredstring

Responses

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"
}

POST /v1/admin/remittances/{id}/pay

Pay a remittance by the bank rail (maker)

Asks a second person to pay a live remittance by the bank rail (POST /v1/admin/approvals/{id}/decide; the requester gets 403 same_approver). Approved, a bank transfer of the net to the biller's settlement account (remit_account, remit_holder) joins the next batch to the sponsor bank: the remittance is paying (payout_id), then paid with paid_by: bank_rail and bank_reference the transfer's EndToEndId when the bank's camt.054 debits it, and the biller receives bill_remittance.paid. If the bank rejects or returns the transfer the remittance is pending again (bill_remittance.payment_failed). 400 for a test-mode remittance (paid by simulation), one not pending, a biller without a NIB and holder, or the rail not enabled. Requires reconciliation:write.

Parameters

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
reason requiredstring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/remittances/{id}/pay" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "daily remittance"
}'
202 Response application/json
{
  "approval": {
    "created_at": "2026-10-10T22:14:59.570339309Z",
    "id": "3KWPlDhVsFz6D7Nlw0sljeII8l1",
    "kind": "bill.remittance_pay",
    "prepared_by": "key_accounts",
    "reason": "pay remittance 3KWPlF7nZdWEWid2kjF1ZQJ718o: 1500.00 MZN to AGUAS *****************3585 — daily",
    "status": "pending",
    "subject_id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
    "tenant_id": "platform"
  },
  "message": "payment requested; a second person must approve it (POST /v1/admin/approvals/{id}/decide)"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "a test-mode remittance is paid by simulation, not by treasury",
  "error": {
    "code": "invalid_request",
    "message": "a test-mode remittance is paid by simulation, not by treasury"
  },
  "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/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

NameInTypeDescription
id requiredpathstring

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

FieldTypeDescription
configobject
driverstring
enabledboolean
modestring
One of: live, test
namestring

Responses

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

NameInTypeDescription
name requiredpathstring The supply connector name or ID.

Request body application/json required

FieldTypeDescription
configobject
driverstring
enabledboolean
modestring
One of: live, test
namestring

Responses

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

NameInTypeDescription
name requiredpathstring

Request body application/json required

FieldTypeDescription
amount_minor requiredinteger
bank_reference requiredstring
reason requiredstring

Responses

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

NameInTypeDescription
name requiredpathstring 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

NameInTypeDescription
name requiredpathstring
dayquerystring (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

NameInTypeDescription
monthquerystring

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"
}

POST /v1/bill-invoices/lookups

Find a bill by its biller and an identifier

A channel whose customer holds a bill finds it by the biller code and any identifier the customer has (letters, digits, spaces, "/", "-", any character), wherever the bill lives (IO-5.4). KulPay's records first: an invoice the biller presented (push, file), by its number as written, the same without spaces or case, or its KulPay ID → source: presented, quote it with invoice.id as invoice_id. Then the biller, the issuer: a lookup biller is asked now through its supply connector with the identifier as the reference → source: lookup, quote it with lookup_id and product. Only a bill the channel may pay is found (the biller active, a product open to the channel, something to pay, the identifier not ambiguous); anything else is 404, and a biller that does not answer is 503 biller_unavailable. Requires bills:read.

Request body application/json required

FieldTypeDescription
biller requiredstring
identifier requiredstring The invoice number as the payer holds it, or its KulPay ID.

Responses

  • 200 The invoice.
  • 404 Not yours or unknown. not_found
  • 503 biller_unavailable: the supplier is down or not answering. It passes: retry after Retry-After seconds.

Request

curl -X POST "$KULPAY_URL/v1/bill-invoices/lookups" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "biller": "AGUAS",
  "identifier": "FT 2026/00123"
}'
200 Response application/json
{
  "account_reference": "CL-200",
  "amount_due": "300.00",
  "biller": "AGUAS",
  "biller_name": "Águas da Região de Maputo",
  "currency": "MZN",
  "due_date": "2026-12-31T00:00:00Z",
  "invoice": {
    "account_reference": "CL-200",
    "amount": "300.00",
    "biller": "AGUAS",
    "currency": "MZN",
    "due_date": "2026-12-31",
    "id": "3KbQ2rLs9tYuW3xZaB5cD7eF8gH",
    "number": "FT 2026/00123",
    "open": "300.00",
    "paid": "0.00",
    "status": "open"
  },
  "source": "presented"
}
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"
}
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-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

NameInTypeDescription
id requiredpathstring 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

NameInTypeDescription
id requiredpathstring The invoice id.

Request body application/json required

FieldTypeDescription
payer_id requiredstring Your payer (id or external_id).
surfacestring
One of: app, web, ussd, agent, other
type requiredstring
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-payments

Your bill payments

Newest first, filtered by status, biller, customer_reference, up to limit (100, at most 500). Requires bills:read.

Parameters

NameInTypeDescription
statusquerystring
billerquerystring
customer_referencequerystring
limitqueryinteger

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

Parameters

NameInTypeDescription
Idempotency-Keyheaderstring (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

FieldTypeDescription
collectobject With funding collect: how KulPay collects the total from the payer.
collect.methodstring
collect.networkstring
collect.phonestring
fundingstring
One of: balance
partner_referencestring
payerobject
payer.emailstring (email)
payer.namestring
payer.phonestring
quote_id requiredstring

Responses

  • 201 Succeeded or failed.
  • 202 Sending or pending.
  • 404 No such quote. not_found
  • 409 quote_used.
  • 422 quote_expired, insufficient_funds, or biller_unavailable: the biller is not accepting payments. pilot_limit while the account is a PSP in pilot: the live transaction breaks the pilot setting named in rule (per_transaction, daily, monthly, currency, settings_missing), checked for either funding before anything is reserved or collected. insufficient_balance
  • 503 biller_unavailable: the supplier is down or not answering. It passes: retry after Retry-After seconds.

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
{
  "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"
}
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_balance application/problem+json
{
  "code": "insufficient_balance",
  "detail": "the balance (10.00 MZN) does not cover 255.00 MZN",
  "error": {
    "code": "insufficient_balance",
    "message": "the balance (10.00 MZN) does not cover 255.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/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. customer_name is masked unless your contract allows full names and KulPay switched them on for you (F3-17); each such lookup is audited.

Request body application/json required

FieldTypeDescription
biller requiredstring
customer_reference requiredstring
product requiredstring

Responses

  • 200 The lookup.
  • 400 invalid_request.
  • 404 No such biller or product open to you. not_found
  • 422 reference_rejected, or biller_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 after Retry-After seconds.

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 supplier and the product version that produced it. 422 amount_out_of_range gives the allowed range. Requires bills:write.

Request body application/json required

FieldTypeDescription
amountstring
billerstring
customer_referencestring
invoice_idstring A presented invoice (from GET /v1/payers/{id}/invoices or POST /v1/bill-invoices/lookups): biller and product may be left out; the amount defaults to the open amount.
lookup_idstring
offerstring
productstring

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; or biller_unavailable: the biller is not accepting payments; a retry cannot succeed.
  • 503 biller_unavailable: the supplier is down or not answering. It passes: retry after Retry-After seconds.

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",
  "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",
  "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",
  "product": "CREDELEC",
  "product_version": 1,
  "quote_expires_at": "2026-10-10T22:24:58.651608397Z",
  "shape": "metered_prepaid",
  "status": "quoted",
  "total": "255.00"
}
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"
}

GET /v1/bill-payments/{id}

A bill payment

With its artifacts once succeeded. Requires bills:read.

Parameters

NameInTypeDescription
id requiredpathstring

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

NameInTypeDescription
id requiredpathstring 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

NameInTypeDescription
id requiredpathstring

Request body application/json required

FieldTypeDescription
tostring
via requiredstring
One of: sms, email

Responses

Request

curl -X POST "$KULPAY_URL/v1/bill-payments/{id}/resend" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "ana@example.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/engagement-digest

Your daily engagement digest

One webhook a day instead of one per engagement event (F3-18): with the digest on, bill_invoice.sent, bill_invoice.reminder_sent, bill_invoice.opened, bill_invoice.pinned and bill_invoice.unpinned are still recorded (the invoice timeline and the events API show them) but not sent as webhooks; biller.engagement_digest arrives the morning after with how many of your invoices were sent, reminded, opened, pinned and paid that day (Africa/Maputo). Payment and lifecycle events keep coming one by one. Requires invoices:read.

Responses

  • 200 {enabled}.
  • 403 Missing or invalid bearer token. unauthorized

Request

curl -X GET "$KULPAY_URL/v1/biller/engagement-digest" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "enabled": true
}
403 Error · unauthorized application/problem+json
{
  "code": "unauthorized",
  "detail": "Missing or invalid API key.",
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key."
  },
  "status": 401,
  "title": "Unauthorized",
  "type": "about:blank"
}

PUT /v1/biller/engagement-digest

Switch the daily engagement digest on or off

One webhook a day instead of one per engagement event (F3-18): with the digest on, bill_invoice.sent, bill_invoice.reminder_sent, bill_invoice.opened, bill_invoice.pinned and bill_invoice.unpinned are still recorded (the invoice timeline and the events API show them) but not sent as webhooks; biller.engagement_digest arrives the morning after with how many of your invoices were sent, reminded, opened, pinned and paid that day (Africa/Maputo). Payment and lifecycle events keep coming one by one. Requires invoices:write.

Request body application/json required

FieldTypeDescription
enabled requiredboolean

Responses

  • 200 {enabled}.
  • 400 The request body or query failed validation. The response body lists each field-level issue. invalid_request

Request

curl -X PUT "$KULPAY_URL/v1/biller/engagement-digest" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "enabled": true
}'
200 Response application/json
{
  "enabled": true
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "The request body failed validation.",
  "error": {
    "code": "invalid_request",
    "errors": [
      {
        "field": "money.amount",
        "message": "must be a positive decimal with at most 2 places"
      }
    ],
    "message": "The request body failed validation."
  },
  "errors": [
    {
      "field": "money.amount",
      "message": "must be a positive decimal with at most 2 places"
    }
  ],
  "status": 400,
  "title": "Bad Request",
  "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

NameInTypeDescription
formatquerystring
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

NameInTypeDescription
statusquerystring
One of: open, partially_paid, paid, cancelled, expired
account_referencequerystring

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

FieldTypeDescription
account_reference requiredstring
amount requiredstring
currency requiredstring
descriptionstring
due_date requiredstring (date)
issue_datestring (date)
linesobject[]
lines[].amountstring
lines[].descriptionstring
number requiredstring
pdf_urlstring (uri)

Responses

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

NameInTypeDescription
number requiredpathstring The biller's invoice number.

Responses

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

NameInTypeDescription
number requiredpathstring The biller's invoice number.

Request body application/json required

FieldTypeDescription
amountstring
due_datestring (date)
statusstring
One of: cancelled

Responses

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

NameInTypeDescription
number requiredpathstring 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

NameInTypeDescription
number requiredpathstring The biller's invoice number.
filenamequerystring

Request body application/pdf required

Responses

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

NameInTypeDescription
number requiredpathstring The biller's invoice number.

Responses

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

NameInTypeDescription
number requiredpathstring 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"
}

GET /v1/biller/notices

Invoice notices to your payers

KulPay can send the payers you enrolled with their consent (lawful_basis consent, consent recorded) their invoice notices itself (F3-15): a new invoice, the reminder three days before it is due, and the payment confirmation — by SMS when the enrolment has a phone number, by email otherwise, in the payer's language. Never in the quiet hours (a notice written from 21:00 to 08:00 Maputo goes out at 08:00), and at most daily_cap a day per payer. Every SMS ends with how to reply STOP and every email with a link that stops them; a stop is recorded (bill_payer.notices_stopped) and shown on GET /v1/biller/payers. Payers enrolled on any other basis receive nothing. Channels keep receiving their webhooks either way. Requires payers:write.

Responses

  • 200 The settings.

Request

curl -X GET "$KULPAY_URL/v1/biller/notices" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "daily_cap": 3,
  "defaults": [
    {
      "body": "{biller}: nova fatura {number} de {amount}, a pagar até {due_date}.",
      "channel": "sms",
      "kind": "invoice_new",
      "language": "pt"
    },
    {
      "body": "{biller}: new invoice {number} for {amount}, due {due_date}.",
      "channel": "sms",
      "kind": "invoice_new",
      "language": "en"
    }
  ],
  "notify_payers": false,
  "quiet_hours": "21:00-08:00 Africa/Maputo",
  "templates": []
}

PUT /v1/biller/notices

Switch invoice notices on or off, and word them

Replaces the whole set: notify_payers (required) and templates, at most one per kind, channel and language; what you do not word uses KulPay's (defaults). KulPay adds the STOP line itself. Records biller.notices_updated. KulPay can send the payers you enrolled with their consent (lawful_basis consent, consent recorded) their invoice notices itself (F3-15): a new invoice, the reminder three days before it is due, and the payment confirmation — by SMS when the enrolment has a phone number, by email otherwise, in the payer's language. Never in the quiet hours (a notice written from 21:00 to 08:00 Maputo goes out at 08:00), and at most daily_cap a day per payer. Every SMS ends with how to reply STOP and every email with a link that stops them; a stop is recorded (bill_payer.notices_stopped) and shown on GET /v1/biller/payers. Payers enrolled on any other basis receive nothing. Channels keep receiving their webhooks either way. Requires payers:write.

Request body application/json required

FieldTypeDescription
notify_payers requiredboolean
templatesobject[]
templates[].body requiredstring At most 250 characters for an SMS, 2000 for an email. Placeholders: {biller}, {number}, {amount}, {due_date}, {account} (masked).
templates[].channel requiredstring
One of: sms, email
templates[].kind requiredstring
One of: invoice_new, invoice_reminder, invoice_paid
templates[].language requiredstring
One of: pt, en
templates[].subjectstring Email only, required there.

Responses

  • 200 The settings.
  • 400 A template is not valid (kind, channel, language, length or an unknown placeholder). invalid_request

Request

curl -X PUT "$KULPAY_URL/v1/biller/notices" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "notify_payers": true,
  "templates": [
    {
      "body": "{biller}: fatura {number} de {amount}, pague até {due_date}.",
      "channel": "sms",
      "kind": "invoice_new",
      "language": "pt"
    }
  ]
}'
200 Response application/json
{
  "daily_cap": 3,
  "defaults": [
    {
      "body": "{biller}: nova fatura {number} de {amount}, a pagar até {due_date}.",
      "channel": "sms",
      "kind": "invoice_new",
      "language": "pt"
    },
    {
      "body": "{biller}: new invoice {number} for {amount}, due {due_date}.",
      "channel": "sms",
      "kind": "invoice_new",
      "language": "en"
    }
  ],
  "notify_payers": true,
  "quiet_hours": "21:00-08:00 Africa/Maputo",
  "templates": []
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "notify_payers is required.",
  "error": {
    "code": "invalid_request",
    "message": "notify_payers is required."
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}

GET /v1/biller/payers

Your enrolled customers

Contacts masked. notices_stopped_at is when the payer stopped the invoice notices KulPay sends (STOP by SMS or the email link, F3-15): enrolling them again does not undo it. 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. With lawful_basis: consent, each payer carries its consent (consent_at, consent_channel, consent_text), kept with the enrolment and listed by GET /v1/biller/payers; a payer without it is rejected on its line. The portal's billing page enrols the same way, one customer or a CSV (F3-13). Each payer can carry language (pt, the default, or en) for the invoice notices KulPay sends when the biller switches them on (PUT /v1/biller/notices, F3-15).

Request body application/json required

FieldTypeDescription
lawful_basis requiredstring
One of: consent, contract, legal_obligation, legitimate_interest
payers requiredobject[]
payers[].account_reference requiredstring
payers[].consent_atstring When the payer consented: YYYY-MM-DD or RFC 3339. Required with lawful_basis consent (F3-13).
payers[].consent_channelstring Where the payer consented.
One of: portal, web, app, ussd, sms, call, paper, counter
payers[].consent_textstring The text the payer accepted.
payers[].emailstring
payers[].languagestring Language of the invoice notices KulPay sends the payer (F3-15); pt by default.
One of: pt, en
payers[].namestring
payers[].phonestring

Responses

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

NameInTypeDescription
ref requiredpathstring 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/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. Payments reversed after an earlier remittance paid them (F3-16) appear as negative lines marked (reversed) and are deducted from the total; such an invoice was reopened with bill_invoice.reopened.

Parameters

NameInTypeDescription
id requiredpathstring

Responses

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/biller/reports/daily

List the daily report runs

The biller's report runs, newest first, with their breaks. Requires invoices:read.

Responses

  • 200 {data, total}.

Request

curl -X GET "$KULPAY_URL/v1/biller/reports/daily" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "biller_id": "3KZmzQ1rS3tU5vW7xY9zA1bC3dE",
      "breaks": 2,
      "breaks_list": [
        {
          "customer_reference": "CL-9",
          "kind": "supplier_only",
          "request_id": "unknown-77",
          "supplier_amount_minor": 30000,
          "supplier_reference": "AG-002"
        },
        {
          "bill_payment_id": "3KZmyF2gH4iJ6kL8mN0oP2qR4sT",
          "customer_reference": "CL-2",
          "kind": "kulpay_only",
          "kulpay_amount_minor": 50000
        }
      ],
      "connector": "AGUAS",
      "created_at": "2026-10-09T06:00:00Z",
      "day": "2026-10-08T22:00:00Z",
      "id": "3KZn0Bv2Cw4Dx6Ey8Fz0Ga2Hb4I",
      "kulpay_count": 2,
      "matched": 1,
      "source": "biller",
      "supplier_count": 2
    }
  ],
  "total": 1
}

POST /v1/biller/reports/daily

Send the day's collections report

The biller's report of the payments it received through KulPay on day (F3-12), as CSV with the header request_id, customer_reference, amount, currency, paid_at, supplier_reference or JSON {"payments": [...]} with the same fields; request_id (the bill payment id, or the request id KulPay sent) or supplier_reference on each payment, 10 000 at most. It is reconciled at once with the biller's bill payments of the day; the answer lists the breaks (supplier_only, kulpay_only, amount_mismatch, review_found, failed_but_delivered), also shown in the portal and to the operators (bill_recon.biller_report). Requires invoices:write.

Parameters

NameInTypeDescription
dayquerystring (date) Default: yesterday.

Request body application/json required

FieldTypeDescription
paymentsobject[]
payments[].amountstring
payments[].currencystring
payments[].customer_referencestring
payments[].paid_atstring (date-time)
payments[].request_idstring
payments[].supplier_referencestring

Responses

  • 200 The run with its breaks.

Request

curl -X POST "$KULPAY_URL/v1/biller/reports/daily" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "payments": [
    {
      "amount": "1500.00",
      "currency": "MZN",
      "customer_reference": "CL-1",
      "paid_at": "2026-10-09T09:00:00Z",
      "request_id": "3KZmxE1fG3hI5jK7lM9nO1pQ3rS",
      "supplier_reference": "AG-001"
    }
  ]
}'
200 Response application/json
{
  "biller_id": "3KZmzQ1rS3tU5vW7xY9zA1bC3dE",
  "breaks": 2,
  "breaks_list": [
    {
      "customer_reference": "CL-9",
      "kind": "supplier_only",
      "request_id": "unknown-77",
      "supplier_amount_minor": 30000,
      "supplier_reference": "AG-002"
    },
    {
      "bill_payment_id": "3KZmyF2gH4iJ6kL8mN0oP2qR4sT",
      "customer_reference": "CL-2",
      "kind": "kulpay_only",
      "kulpay_amount_minor": 50000
    }
  ],
  "connector": "AGUAS",
  "created_at": "2026-10-09T06:00:00Z",
  "day": "2026-10-08T22:00:00Z",
  "id": "3KZn0Bv2Cw4Dx6Ey8Fz0Ga2Hb4I",
  "kulpay_count": 2,
  "matched": 1,
  "source": "biller",
  "supplier_count": 2
}

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

NameInTypeDescription
categoryquerystring
One of: electricity, water, telecom, tv, internet, education, insurance, government, other
qquerystring

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",
              "name": "Pacote Básico",
              "price": "750.00",
              "validity": "30d"
            },
            {
              "code": "TVPREMIUM",
              "name": "Pacote Premium",
              "price": "2500.00",
              "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

NameInTypeDescription
code requiredpathstring 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",
          "name": "Pacote Básico",
          "price": "750.00",
          "validity": "30d"
        },
        {
          "code": "TVPREMIUM",
          "name": "Pacote Premium",
          "price": "2500.00",
          "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"
}

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). Requires bills:write.

Request body application/json required

FieldTypeDescription
emailstring (email)
external_id requiredstring
namestring
phonestring

Responses

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.macuacua@example.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

NameInTypeDescription
id requiredpathstring 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.cossa@example.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

NameInTypeDescription
id requiredpathstring 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

NameInTypeDescription
id requiredpathstring 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. Opting in again after opting out reactivates the subscription. Records bill_subscription.opted_in. Requires bills:write.

Parameters

NameInTypeDescription
id requiredpathstring The KulPay payer id or your external_id.

Request body application/json required

FieldTypeDescription
account_reference requiredstring
aliasstring
biller requiredstring
consent requiredobject
consent.accepted requiredboolean
consent.text_version requiredstring

Responses

  • 201 The subscription (active or pending_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): nothing of that biller account is delivered any more, and a later enrolment by the biller does not subscribe the payer again. Optional ?reason=. Records bill_subscription.opted_out. Requires bills:write.

Parameters

NameInTypeDescription
id requiredpathstring The KulPay payer id or your external_id.
sid requiredpathstring
reasonquerystring

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"
}