API Live

API reference

Bill payments

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

GET /pay/invoice/{token}

An invoice's payment page

Public, no key: the token in the invoice's pay_url is the capability (BIL-5.9). Shows the biller, the invoice, the open amount and the payer's fee, and the RF reference to pay at a partner bank. 404 for an unknown or reissued token.

Parameters

NameInTypeDescription
token requiredpathstring

Responses

  • 200 The HTML page.
  • 404 No such link: the unavailable-link page.

Request

curl -X GET "$KULPAY_URL/pay/invoice/{token}" \
  -H "Authorization: Bearer $KULPAY_KEY"

POST /pay/invoice/{token}

Pay an invoice from its page

Form post (with the page's CSRF token) of the open amount, or amount when the biller takes partial payments: KulPay quotes it, collecting on the biller's behalf on its platform account, and redirects (303) to the hosted checkout for the total, where the payer chooses M-Pesa, e-Mola, a card and so on. Once the checkout payment succeeds the invoice is paid (funding collect) and the biller is told with bill_invoice.payment_received.

Parameters

NameInTypeDescription
token requiredpathstring

Request body application/x-www-form-urlencoded

FieldTypeDescription
_csrfstring
amountstring

Responses

  • 303 To /checkout/{id}.
  • 404 No such link: the unavailable-link page.
  • 422 The amount is not accepted, or the invoice takes no more payments: the page again, with the reason.

Request

curl -X POST "$KULPAY_URL/pay/invoice/{token}" \
  -H "Authorization: Bearer $KULPAY_KEY"

GET /pay/invoice/{token}/qr.svg

The payment page's QR

Public, as the payment page: the invoice's QR as SVG, for a payer who opened the page on a computer and pays with the app on their phone (BIL-5.12).

Parameters

NameInTypeDescription
token requiredpathstring

Responses

  • 200 The QR.
  • 404 No such link, or the invoice takes no payments.

Request

curl -X GET "$KULPAY_URL/pay/invoice/{token}/qr.svg" \
  -H "Authorization: Bearer $KULPAY_KEY"

PUT /v1/admin/accounts/{id}/bill-balance-alert

A channel's low-balance alert

The balance below which a channel paying bills from its balance is sent bill_balance.low (once, until it is back above). low_balance_minor: 0 turns it off. Requires accounts:write.

Parameters

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-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. An Idempotency-Key on this request binds the key to it as usual, but the success response holds a secret and is never kept (IG-30): the same key and request again is 409 idempotency_replay_unavailable (never the secret a second time; list the resources to find the one it made), another request under the key 409 idempotency_key_reused.

Parameters

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"
}
409 Error · idempotency_replay_unavailable application/problem+json
{
  "code": "idempotency_replay_unavailable",
  "detail": "A request with this Idempotency-Key already succeeded. Its response held a secret that is shown once and not kept, so it cannot be replayed: list the resources to find the one it created, and revoke it if its secret was lost.",
  "error": {
    "code": "idempotency_replay_unavailable",
    "message": "A request with this Idempotency-Key already succeeded. Its response held a secret that is shown once and not kept, so it cannot be replayed: list the resources to find the one it created, and revoke it if its secret was lost."
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

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

Resolve a bill payment in review

A payment goes to review when querying its supplier with the same request id has not settled it within 24 hours (BIL-2.5). An operator records what the supplier did: succeeded with the supplier's reference and, if one was issued, the token or PIN (sealed in the approval), or failed. A second operator approves; the payment then succeeds (the reserve settled, bill_payment.succeeded) or fails (released, bill_payment.failed), with bill_payment.resolved. Requires admin:write.

Parameters

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

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": "apoioexample.co.mz",
      "support_phone": "+258841234567",
      "updated_at": "2026-09-18T10:42:11Z"
    },
    {
      "allow_overpay": false,
      "allow_partial": true,
      "category": "water",
      "code": "AGUAS_MAPUTO",
      "country": "MZ",
      "created_at": "2026-04-11T09:00:00Z",
      "id": "3KUXR1mVq8sLd2tPfGhZa7YwKe4",
      "invoice_expiry_days": 30,
      "name": "Águas da Região de Maputo",
      "presentment_modes": [
        "push",
        "lookup"
      ],
      "remit_account": "000800001234567890157",
      "remit_bank": "BCI",
      "remit_frequency": "daily",
      "status": "active",
      "support_email": "clientesexample.co.mz",
      "tenant_id": "3KUXS7nBw2cQe5rTgHjXb9ZvLf5",
      "updated_at": "2026-10-01T14:20:37Z"
    }
  ],
  "total": 2
}

POST /v1/admin/billers

Register a biller

Creates a biller as a draft (BIL-1.1); channels see it once a second operator activates it. Records biller.created. Requires connectors:write.

Request body application/json required

FieldTypeDescription
categorystring
One of: electricity, water, telecom, tv, internet, education, insurance, government, other
codestring
countrystring
logo_urlstring (uri)
namestring
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; 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
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"
}

GET /v1/admin/billers/{code}/accounts/{reference}/qr

A meter's or account's QR (operators)

A QR for a meter or customer account of a biller with no portal (BIL-5.13): a Credelec meter, an airtime number. Its page buys for the meter through the hosted checkout (amount and the payer's phone, which gets the token by SMS). As GET /v1/biller/accounts/{reference}/qr. Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
reference requiredpathstring The customer account or meter number: 1 to 40 printable characters, no spaces.
productquerystring The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it.

Responses

  • 200 The account's QR.
  • 400 invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active.
  • 404 No such biller. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/accounts/{reference}/qr" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "biller": "AGUAS",
  "mode": "live",
  "png_url": "/v1/biller/accounts/CL-001234/qr.png",
  "reference": "CL-0042",
  "svg_url": "/v1/biller/accounts/CL-001234/qr.svg",
  "url": "https://pay.kulpay.co.mz/pay/account/bB9BR1VBUx8fQ0wtMDA0Mg.3oT2Lw9sQe1Zk7mN"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "no product of AGUAS takes CL-****2 as a reference",
  "error": {
    "code": "invalid_request",
    "message": "no product of AGUAS takes CL-****2 as a reference"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/billers/{code}/accounts/{reference}/qr.png

A meter's or account's QR, PNG (operators)

A QR for a meter or customer account of a biller with no portal (BIL-5.13): a Credelec meter, an airtime number. Its page buys for the meter through the hosted checkout (amount and the payer's phone, which gets the token by SMS). As GET /v1/biller/accounts/{reference}/qr. Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
reference requiredpathstring The customer account or meter number: 1 to 40 printable characters, no spaces.
productquerystring The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 400 invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active.
  • 404 No such biller. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/accounts/{reference}/qr.png" \
  -H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "no product of AGUAS takes CL-****2 as a reference",
  "error": {
    "code": "invalid_request",
    "message": "no product of AGUAS takes CL-****2 as a reference"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/billers/{code}/accounts/{reference}/qr.svg

A meter's or account's QR, rendered (operators)

A QR for a meter or customer account of a biller with no portal (BIL-5.13): a Credelec meter, an airtime number. Its page buys for the meter through the hosted checkout (amount and the payer's phone, which gets the token by SMS). As GET /v1/biller/accounts/{reference}/qr. Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
reference requiredpathstring The customer account or meter number: 1 to 40 printable characters, no spaces.
productquerystring The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 400 invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active.
  • 404 No such biller. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/accounts/{reference}/qr.svg" \
  -H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "no product of AGUAS takes CL-****2 as a reference",
  "error": {
    "code": "invalid_request",
    "message": "no product of AGUAS takes CL-****2 as a reference"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

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

Configure a product

A product (BIL-1.2): its shape, amount rule, customer reference, supply connectors in priority order, commercial model and fee. The fee is borne by the payer or by the biller, never both (422 fee_bearer_conflict): a payer fee cannot come with a supplier discount, the agent model has no discount, and on the principal model with the biller bearing, the discount is the fee. Records bill_product.created. Requires connectors:write.

Parameters

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
purchase_modesstring[] How the product may be bought: `fixed_catalog` (an offer from its catalogue) and/or `variable_amount` (an amount under `amount_rule`). Both by default; a catalogue-only product needs no `amount_rule`.
reference_examplestring
reference_labelstring
reference_patternstring A regular expression the reference must match.
routableboolean A lookup that names no biller is sent to this biller when the reference matches reference_pattern (BIL-5.14); needs reference_pattern.
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
purchase_modesstring[] How the product may be bought: `fixed_catalog` (an offer from its catalogue) and/or `variable_amount` (an amount under `amount_rule`). Both by default; a catalogue-only product needs no `amount_rule`.
reference_examplestring
reference_labelstring
reference_patternstring A regular expression the reference must match.
routableboolean A lookup that names no biller is sent to this biller when the reference matches reference_pattern (BIL-5.14); needs reference_pattern.
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"
}

GET /v1/admin/billers/{code}/products/{product}/integration

A product's notification and report (admin)

The product's own integration with its LSP in a mode (BIL-5.16), secrets masked; defaults when none was set. Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test

Responses

  • 200 The integration.
  • 404 No such biller or product. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "biller": "AGUAS",
  "fields": [
    "event",
    "notification_id",
    "payment_id",
    "request_id",
    "mode",
    "biller",
    "biller_name",
    "product",
    "supplier_product",
    "reference",
    "invoice_number",
    "amount",
    "payer_fee",
    "total",
    "currency",
    "amount_minor",
    "total_minor",
    "paid_at",
    "channel_id",
    "channel_name",
    "funding",
    "partner_reference",
    "supplier_reference",
    "receipt_url"
  ],
  "mode": "test",
  "notification": {
    "auth": "hmac",
    "content_type": "application/xml",
    "enabled": true,
    "headers": {
      "X-Biller-Code": "AGUAS-01"
    },
    "method": "POST",
    "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
    "url": "https://lsp.example/kulpay/pagamentos"
  },
  "product": "FACTURA",
  "report": {
    "auth": "bearer",
    "columns": [
      {
        "field": "paid_at",
        "header": "DATA",
        "width": 10
      },
      {
        "field": "invoice_number",
        "header": "FACTURA",
        "width": 12
      },
      {
        "align": "right",
        "field": "total",
        "header": "VALOR",
        "width": 10
      }
    ],
    "date_format": "DD/MM/YYYY",
    "decimal_separator": ",",
    "enabled": true,
    "file_name": "AGUAS_{date}.dat",
    "format": "txt",
    "header": true,
    "last_day": "2026-10-19",
    "line_ending": "lf",
    "url": "https://lsp.example/kulpay/reconciliacao"
  },
  "updated_at": "2026-10-20T08:00:00Z",
  "version": 3
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/admin/billers/{code}/products/{product}/integration

Set a product's notification and report (admin)

Sets the payment notification (URL, body template, content type, auth, headers) and the daily report (format, columns, file name, URL) of a product in a mode (BIL-5.16). Fields left out keep their value. Each change is a new version (bill_product.integration_set). The change takes effect at once, in live too, with no second approval (owner decision 2026-10-20). Requires connectors:write.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test

Request body application/json required

FieldTypeDescription
notificationobject The payment notification: every payment of the product that succeeds is sent to `url`, its body rendered from `template` (Go text/template over the fields; helpers `json`, `xml`, `url` escape a value, `date "DD/MM/YYYY HH:mm"` formats paid_at, `cents` drops the decimal point). Checked when set: the body must be what `content_type` says. Retried for 24 h; kept for resends.
notification.authobject How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only.
notification.auth.passwordstring
notification.auth.secretstring
notification.auth.tokenstring
notification.auth.typestring
One of: none, bearer, basic, hmac
notification.auth.usernamestring
notification.content_typestring
One of: application/json, application/xml, application/x-www-form-urlencoded, text/plain
notification.enabledboolean
notification.headersobject Up to 10 static headers; not Authorization, Content-Type or KulPay-*.
notification.methodstring
One of: POST, PUT
notification.templatestring
notification.urlstring (uri)
reportobject The daily report: the product's payments of a day, in the LSP's format and file extension; downloadable, and sent to `url` the next day.
report.authobject How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only.
report.auth.passwordstring
report.auth.secretstring
report.auth.tokenstring
report.auth.typestring
One of: none, bearer, basic, hmac
report.auth.usernamestring
report.columnsobject[]
report.columns[].alignstring
One of: left, right
report.columns[].field requiredstring
One of: event, notification_id, payment_id, request_id, mode, biller, biller_name, product, supplier_product, reference, invoice_number, amount, payer_fee, total, currency, amount_minor, total_minor, paid_at, channel_id, channel_name, funding, partner_reference, supplier_reference, receipt_url
report.columns[].headerstring
report.columns[].widthinteger txt (fixed width) only.
report.date_formatstring
report.decimal_separatorstring
One of: ., ,
report.enabledboolean
report.file_namestring Placeholders {biller}, {product}, {date}, {mode}; the extension is the one given, else the format's.
report.formatstring
One of: csv, tsv, psv, txt, json, xml, xlsx
report.headerboolean
report.line_endingstring
One of: lf, crlf
report.urlstring (uri)

Responses

Request

curl -X PUT "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "notification": {
    "auth": {
      "secret": "<o seu segredo>",
      "type": "hmac"
    },
    "content_type": "application/xml",
    "enabled": true,
    "headers": {
      "X-Biller-Code": "AGUAS-01"
    },
    "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
    "url": "https://lsp.example/kulpay/pagamentos"
  },
  "report": {
    "auth": {
      "token": "<o seu token>",
      "type": "bearer"
    },
    "columns": [
      {
        "field": "paid_at",
        "header": "DATA",
        "width": 10
      },
      {
        "field": "invoice_number",
        "header": "FACTURA",
        "width": 12
      },
      {
        "align": "right",
        "field": "total",
        "header": "VALOR",
        "width": 10
      }
    ],
    "date_format": "DD/MM/YYYY",
    "decimal_separator": ",",
    "enabled": true,
    "file_name": "AGUAS_{date}.dat",
    "format": "txt",
    "url": "https://lsp.example/kulpay/reconciliacao"
  }
}'
200 Response application/json
{
  "biller": "AGUAS",
  "fields": [
    "event",
    "notification_id",
    "payment_id",
    "request_id",
    "mode",
    "biller",
    "biller_name",
    "product",
    "supplier_product",
    "reference",
    "invoice_number",
    "amount",
    "payer_fee",
    "total",
    "currency",
    "amount_minor",
    "total_minor",
    "paid_at",
    "channel_id",
    "channel_name",
    "funding",
    "partner_reference",
    "supplier_reference",
    "receipt_url"
  ],
  "mode": "test",
  "notification": {
    "auth": "hmac",
    "content_type": "application/xml",
    "enabled": true,
    "headers": {
      "X-Biller-Code": "AGUAS-01"
    },
    "method": "POST",
    "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
    "url": "https://lsp.example/kulpay/pagamentos"
  },
  "product": "FACTURA",
  "report": {
    "auth": "bearer",
    "columns": [
      {
        "field": "paid_at",
        "header": "DATA",
        "width": 10
      },
      {
        "field": "invoice_number",
        "header": "FACTURA",
        "width": 12
      },
      {
        "align": "right",
        "field": "total",
        "header": "VALOR",
        "width": 10
      }
    ],
    "date_format": "DD/MM/YYYY",
    "decimal_separator": ",",
    "enabled": true,
    "file_name": "AGUAS_{date}.dat",
    "format": "txt",
    "header": true,
    "last_day": "2026-10-19",
    "line_ending": "lf",
    "url": "https://lsp.example/kulpay/reconciliacao"
  },
  "updated_at": "2026-10-20T08:00:00Z",
  "version": 3
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "notification: the template does not render well-formed XML (use {{xml .field}} for values)",
  "error": {
    "code": "invalid_request",
    "message": "notification: the template does not render well-formed XML (use {{xml .field}} for values)"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/billers/{code}/products/{product}/integration/deliveries

A product's notifications and reports sent (admin)

The deliveries to the LSP, newest first: status pending (retrying), delivered or exhausted, attempts, the endpoint's last answer. Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test
kindquerystring
One of: notification, report

Responses

  • 200 The deliveries.
  • 404 No such biller or product. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/deliveries" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "attempts": 1,
      "content_type": "application/xml",
      "created_at": "2026-10-20T08:00:00Z",
      "delivered_at": "2026-10-20T08:00:01Z",
      "duration_ms": 84,
      "id": "3KvxDelivery00000000000001",
      "integration_id": "3KvxIntegration0000000001",
      "kind": "notification",
      "method": "POST",
      "mode": "test",
      "next_attempt_at": "2026-10-20T08:00:00Z",
      "response_code": 200,
      "status": "delivered",
      "subject_id": "3KvxPayment000000000000001",
      "updated_at": "2026-10-20T08:00:01Z",
      "url": "https://lsp.example/kulpay/pagamentos"
    }
  ]
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}

One delivery and its exact body (admin)

What was sent, byte for byte: body (text) or body_base64 (xlsx). Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test
id requiredpathstring The delivery id.

Responses

  • 200 The delivery.
  • 404 No such biller or product. not_found

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "body": "<pagamento><referencia>CL-1</referencia><valor>151000</valor><data>19/10/2026 12:00</data></pagamento>",
  "delivery": {
    "attempts": 1,
    "content_type": "application/xml",
    "created_at": "2026-10-20T08:00:00Z",
    "delivered_at": "2026-10-20T08:00:01Z",
    "duration_ms": 84,
    "id": "3KvxDelivery00000000000001",
    "integration_id": "3KvxIntegration0000000001",
    "kind": "notification",
    "method": "POST",
    "mode": "test",
    "next_attempt_at": "2026-10-20T08:00:00Z",
    "response_code": 200,
    "status": "delivered",
    "subject_id": "3KvxPayment000000000000001",
    "updated_at": "2026-10-20T08:00:01Z",
    "url": "https://lsp.example/kulpay/pagamentos"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}/retry

Send a delivery again (admin)

Sends a pending or exhausted delivery again now, the same body. Requires connectors:write.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test
id requiredpathstring The delivery id.

Responses

  • 200 The delivery after the attempt.
  • 404 No such biller or product. not_found

Request

curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/deliveries/{id}/retry" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "body": "<pagamento>…</pagamento>",
  "delivery": {
    "attempts": 1,
    "content_type": "application/xml",
    "created_at": "2026-10-20T08:00:00Z",
    "delivered_at": "2026-10-20T08:00:01Z",
    "duration_ms": 84,
    "id": "3KvxDelivery00000000000001",
    "integration_id": "3KvxIntegration0000000001",
    "kind": "notification",
    "method": "POST",
    "mode": "test",
    "next_attempt_at": "2026-10-20T08:00:00Z",
    "response_code": 200,
    "status": "delivered",
    "subject_id": "3KvxPayment000000000000001",
    "updated_at": "2026-10-20T08:00:01Z",
    "url": "https://lsp.example/kulpay/pagamentos"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/billers/{code}/products/{product}/integration/preview

Preview a notification's body (admin)

Renders a template (the one given, else the product's) for a sample payment, as the LSP would receive it. Requires connectors:write.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test

Request body application/json required

FieldTypeDescription
content_typestring
templatestring

Responses

Request

curl -X POST "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/integration/preview" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "content_type": "application/xml",
  "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>"
}'
200 Response application/json
{
  "body": "<pagamento><referencia>EX-123456</referencia><valor>126000</valor><data>19/10/2026 16:30</data></pagamento>",
  "content_type": "application/xml"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "template: unexpected \"}\" in operand",
  "error": {
    "code": "invalid_request",
    "message": "template: unexpected \"}\" in operand"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/admin/billers/{code}/products/{product}/limits

Change a product's daily limits

Per day (Maputo time): payments to one customer reference (reference_daily_count, reference_daily_minor) and the amount one channel sells (channel_daily_minor); 0 removes a limit (BIL-6.4). A second operator approves. Quotes and payments beyond a limit are refused with 422 limit_exceeded. Requires connectors:write.

Parameters

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

GET /v1/admin/billers/{code}/products/{product}/reports/{day}

A product's report for a day (admin)

The product's payments that succeeded on the day (Maputo), in its report format and file name (Content-Disposition). Requires connectors:read.

Parameters

NameInTypeDescription
code requiredpathstring The biller code.
product requiredpathstring The product code (KulPay's).
modequerystring The mode whose integration (the LSP's test and live endpoints differ). Default: the key's.
One of: live, test
day requiredpathstring (date) The day, YYYY-MM-DD, Maputo time.

Responses

Request

curl -X GET "$KULPAY_URL/v1/admin/billers/{code}/products/{product}/reports/{day}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response text/plain
DATA      FACTURA          VALOR
19/10/2026F-N1           1510,00
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "the day is YYYY-MM-DD",
  "error": {
    "code": "invalid_request",
    "message": "the day is YYYY-MM-DD"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

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

Remit a biller now

Creates the biller's remittances of what KulPay collected for it on the agent model and has not remitted, less the fees it bears, one per mode and currency (BIL-5.7). Billers with a bank account and a remit_frequency are remitted daily or weekly by job. Records bill_remittance.created (a webhook to the biller). Requires reconciliation:write.

Parameters

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/reference-routes

Where a reference would be routed (operators)

Which billers a reference lookup that names no biller would ask (BIL-5.14): the active billers whose routable products' reference_pattern matches, for a channel when given. too_many when more than 5 match (the lookup answers biller_required). Requires connectors:read.

Parameters

NameInTypeDescription
reference requiredquerystring
channelquerystring A channel account: only products open to it.

Responses

  • 200 The route.
  • 400 A reference of 3 to 128 characters is required. invalid_request

Request

curl -X GET "$KULPAY_URL/v1/admin/reference-routes" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "billers": [
    {
      "biller": "EDM",
      "biller_name": "Electricidade de Moçambique",
      "products": [
        "CREDELEC"
      ]
    },
    {
      "biller": "LUZ",
      "biller_name": "Luz do Norte",
      "products": [
        "CONTA"
      ]
    }
  ],
  "reference": "01123456782",
  "rf": false,
  "then": "these billers, in parallel; the deposit resolver if none knows it",
  "too_many": false
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "a reference is 3 to 128 characters",
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}

GET /v1/admin/remittances

Remittances to billers

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

Parameters

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 as paid

The bank transfer that paid the biller: its reference and a reason. A second person approves; the biller payable is then cleared against client funds and the biller receives bill_remittance.paid. Requires reconciliation:write.

Parameters

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

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

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-invoices/{id}/qr.png

A bill's QR as PNG (channel)

The QR of a bill you may pay, to show your customer (BIL-5.12): it encodes the invoice's payment link (qr.url), which any phone camera opens. No personal data. Requires bills:read.

Parameters

NameInTypeDescription
id requiredpathstring The invoice id.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 404 No such invoice, or one you may not pay. not_found
  • 409 invoice_closed: a paid, cancelled or expired invoice has no QR.

Request

curl -X GET "$KULPAY_URL/v1/bill-invoices/{id}/qr.png" \
  -H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
  "code": "invoice_closed",
  "detail": "invoice F-Q takes no payments, so it has no QR",
  "error": {
    "code": "invoice_closed",
    "message": "invoice F-Q takes no payments, so it has no QR"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/bill-invoices/{id}/qr.svg

A bill's QR (channel)

The QR of a bill you may pay, to show your customer (BIL-5.12): it encodes the invoice's payment link (qr.url), which any phone camera opens. No personal data. Requires bills:read.

Parameters

NameInTypeDescription
id requiredpathstring The invoice id.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 404 No such invoice, or one you may not pay. not_found
  • 409 invoice_closed: a paid, cancelled or expired invoice has no QR.

Request

curl -X GET "$KULPAY_URL/v1/bill-invoices/{id}/qr.svg" \
  -H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
  "code": "invoice_closed",
  "detail": "invoice F-Q takes no payments, so it has no QR",
  "error": {
    "code": "invoice_closed",
    "message": "invoice F-Q takes no payments, so it has no QR"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/bill-payments

Your bill payments

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

Parameters

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 as issued, through the supply connector that issued it, or not at all: a quote whose connector was disabled or withdrawn from the product, or whose terms no longer match, is refused before anything is reserved (409 quote_invalidated), and is never sent through another supplier; should that happen after acceptance but before sending, the payment fails with quote_invalidated and the reserve is released. payment_status (the money) and fulfilment (the supplier leg, with the evidence that settled it) are reported separately. A quote is paid once (409 quote_used); an expired one is refused (422 quote_expired); 422 insufficient_funds leaves the quote payable. Requires bills:write and an Idempotency-Key. The launch capability matrix (plan phase 10) holds the route first: one it does not enable is 422 route_not_enabled, and anything off_net 422 movement_scope_disabled, before any limit is reserved or provider called.

Parameters

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
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; or quote_invalidated: the quote no longer holds as issued (its supplier was disabled or withdrawn, or its terms changed). Request a new quote.
  • 422 quote_expired, insufficient_funds, or biller_unavailable: the biller is not accepting payments. Or the route is not in the launch matrix (route_not_enabled, movement_scope_disabled).
  • 503 biller_unavailable: the supplier is down or not answering. It passes: retry 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
{
  "accepted_at": "2026-10-10T22:14:58.70112Z",
  "amount": "250.00",
  "amount_minor": 25000,
  "artifacts": [
    {
      "type": "receipt",
      "value": "FK2026101045631213"
    },
    {
      "meta": {
        "krn": "1",
        "sgc": "600123",
        "ti": "1"
      },
      "type": "token",
      "unit_type": "kWh",
      "units": "25.00",
      "value": "69556183477675547957"
    }
  ],
  "biller": "EDM",
  "biller_name": "EDM",
  "breakdown": {
    "lines": [
      {
        "amount": "250.00",
        "concept": "Unit charge per kWh",
        "price": "10.00",
        "units": "25.00"
      }
    ],
    "unit_type": "kWh",
    "units": "25.00"
  },
  "completed_at": "2026-10-10T22:14:58.744497415Z",
  "created_at": "2026-10-10T22:14:58.651626029Z",
  "currency": "MZN",
  "customer_name": "C****** T**** 7***",
  "customer_reference": "01234567890",
  "fee_bearer": "payer",
  "fulfilment": {
    "attempts": 1,
    "evidence": {
      "authenticated_by": "connector_session",
      "connector_id": "3KWPl3x9eQf0yX1oVJm2aQ8bLtR",
      "driver": "edm",
      "observed_at": "2026-10-10T22:14:58.744497415Z",
      "outcome": "succeeded",
      "request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
      "result_sha256": "5c1f0e6b2d9a4c7e8f3a1b2c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70",
      "source": "fulfil_response",
      "supplier_reference": "FK2026101045631213"
    },
    "request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
    "status": "succeeded",
    "supplier_reference": "FK2026101045631213"
  },
  "funding": "balance",
  "id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
  "mode": "test",
  "object": "bill_payment",
  "participants": [
    {
      "account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
      "name": "Tenant B",
      "role": "initiating_psp"
    },
    {
      "account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
      "name": "Tenant B",
      "role": "from_psp",
      "settlement": "prefunded_balance"
    }
  ],
  "partner_reference": "POS-9",
  "payer_fee": "5.00",
  "payer_fee_minor": 500,
  "payment_status": "captured",
  "product": "CREDELEC",
  "product_version": 1,
  "purchase_mode": "variable_amount",
  "quote_expires_at": "2026-10-10T22:24:58.651608397Z",
  "receipt_url": "/bills/receipt/3KWPl4pk0XxRI4TEVI6avRgrj1y?t=plain%3AYmlsbC1yZWNlaXB0fDNLV1BsNHBrMFh4Ukk0VEVWSTZhdlJncmoxeQ",
  "request_id": "kp-3KWPl4pk0XxRI4TEVI6avRgrj1y-1",
  "shape": "metered_prepaid",
  "status": "succeeded",
  "supplier_connector": "edm-live",
  "supplier_reference": "FK2026101045631213",
  "total": "255.00",
  "total_minor": 25500
}
202 Response application/json
{
  "amount": "250.00",
  "artifacts": [
    {
      "meta": {
        "key": "1250.00"
      },
      "serial": "string",
      "type": "token",
      "unit_type": "string",
      "units": "string",
      "value": "1250.00"
    }
  ],
  "biller": "EDM",
  "biller_name": "Electricidade de Moçambique",
  "breakdown": {
    "debt": "250.00",
    "lines": [
      {
        "amount": "250.00",
        "concept": "string",
        "price": "string",
        "units": "string"
      }
    ],
    "unit_type": "kWh",
    "units": "25.00"
  },
  "completed_at": "2026-10-10T09:30:00Z",
  "created_at": "2026-10-10T09:30:00Z",
  "currency": "MZN",
  "customer_name": "Electricidade de Moçambique",
  "customer_reference": "2003014567",
  "failure_code": "EDM",
  "failure_message": "Pagamento de energia, outubro",
  "fee_bearer": "payer",
  "funding": "balance",
  "id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "mode": "test",
  "object": "bill_payment",
  "offer": "string",
  "partner_reference": "2003014567",
  "payer_fee": "250.00",
  "product": "CREDELEC",
  "product_version": 1,
  "quote_expires_at": "2026-10-10T09:30:00Z",
  "request_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
  "shape": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "status": "quoted",
  "supplier_reference": "2003014567",
  "total": "250.00"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such bill payment or quote",
  "error": {
    "code": "not_found",
    "message": "no such bill payment or quote"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · quote_used application/problem+json
{
  "code": "quote_used",
  "detail": "this quote was already paid (bill payment 3KWPl4pk0XxRI4TEVI6avRgrj1y is succeeded)",
  "error": {
    "code": "quote_used",
    "message": "this quote was already paid (bill payment 3KWPl4pk0XxRI4TEVI6avRgrj1y is succeeded)"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}
422 Error · insufficient_funds application/problem+json
{
  "code": "insufficient_balance",
  "detail": "the balance (10.00 MZN) does not cover 255.00 MZN",
  "error": {
    "code": "insufficient_funds",
    "message": "the balance (10.00 MZN) does not cover 255.00 MZN"
  },
  "legacy_code": "insufficient_funds",
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
  "code": "biller_unavailable",
  "detail": "the supplier is not answering; try again shortly",
  "error": {
    "code": "biller_unavailable",
    "message": "the supplier is not answering; try again shortly"
  },
  "status": 503,
  "title": "Service Unavailable",
  "type": "about:blank"
}

POST /v1/bill-payments/lookups

Look up a customer reference

Asks the product's supplier who a meter, account or number belongs to (BIL-2.2) before any money is taken. A found reference returns a lookup_id valid for 15 minutes, the customer's name masked, the amount due for postpaid references and the limits a payment must respect; a repeat within that time is answered from the lookup. status: not_found keeps nothing. 400 invalid_request when the reference does not match the product's pattern; 503 biller_unavailable when no supplier answers. Requires bills:read.

Request body application/json required

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 supply connector (supplier_connector), the product version and the product's code at the supplier that produced it, with a digest of its terms: it is paid as issued or not at all. purchase_mode says whether it was priced from the catalogue (offer) or as an amount; a product may sell either or both (purchase_modes on the listing). To price it again, refresh it (POST /v1/bill-payments/quotes/{id}/refresh): a new quote from the same connector, the old one unchanged. 422 amount_out_of_range gives the allowed range. Requires bills:write. When the provider states them, the quote also carries the bill's open amount (outstanding_minor) and whether it may be paid in part (allow_partial), as stated at quote time. When the provider describes the bill, the quote carries bill_details (holder, service address, billing period, issue and due dates) for display; they are not terms.

Request body application/json required

FieldTypeDescription
amountstring
biller requiredstring
customer_referencestring
lookup_idstring
offerstring An offer code from the product's catalogue (`fixed_catalog`); leave out for an amount (`variable_amount`).
product requiredstring

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",
  "amount_minor": 25000,
  "biller": "EDM",
  "biller_name": "EDM",
  "breakdown": {
    "lines": [
      {
        "amount": "250.00",
        "concept": "Unit charge per kWh",
        "price": "10.00",
        "units": "25.00"
      }
    ],
    "unit_type": "kWh",
    "units": "25.00"
  },
  "created_at": "2026-10-10T22:14:58.651626029Z",
  "currency": "MZN",
  "customer_name": "C****** T**** 7***",
  "customer_reference": "01234567890",
  "fee_bearer": "payer",
  "fulfilment": {
    "attempts": 0,
    "status": "not_started"
  },
  "id": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
  "mode": "test",
  "object": "bill_payment",
  "participants": [
    {
      "account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
      "name": "Tenant B",
      "role": "initiating_psp"
    },
    {
      "account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
      "name": "Tenant B",
      "role": "from_psp",
      "settlement": "prefunded_balance"
    }
  ],
  "payer_fee": "5.00",
  "payer_fee_minor": 500,
  "payment_status": "none",
  "product": "CREDELEC",
  "product_version": 1,
  "purchase_mode": "variable_amount",
  "quote_expires_at": "2026-10-10T22:24:58.651608397Z",
  "shape": "metered_prepaid",
  "status": "quoted",
  "supplier_connector": "edm-live",
  "total": "255.00",
  "total_minor": 25500
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "amount must be a positive decimal",
  "error": {
    "code": "invalid_request",
    "message": "amount must be a positive decimal"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
422 Error · amount_out_of_range application/problem+json
{
  "code": "amount_out_of_range",
  "detail": "the amount must be between 20.00 MZN and 50000.00 MZN",
  "error": {
    "code": "amount_out_of_range",
    "message": "the amount must be between 20.00 MZN and 50000.00 MZN"
  },
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
  "code": "biller_unavailable",
  "detail": "the supplier is not answering; try again shortly",
  "error": {
    "code": "biller_unavailable",
    "message": "the supplier is not answering; try again shortly"
  },
  "status": 503,
  "title": "Service Unavailable",
  "type": "about:blank"
}

POST /v1/bill-payments/quotes/{id}/refresh

Refresh a bill quote

Prices a quote again at the supply connector that issued it (design: a refresh creates a new quotation) and returns a new quote, with refreshed_from naming the old one. The old quote is never changed, whatever its state (quoted, expired, paid): what a payer accepted stays as accepted. The reference is looked up again at the same connector when the product requires it; the offer or amount is the old quote's. If that connector is disabled or no longer sells the product, the refresh is refused (409 quote_invalidated or 503 biller_unavailable) rather than priced elsewhere: request a new quote instead. An invoice is quoted again with invoice_id. Requires bills:write.

Parameters

NameInTypeDescription
id requiredpathstring The quote (bill payment) to refresh.

Responses

  • 201 The new quote, a bill payment in quoted.
  • 400 invalid_request: an invoice quote (quote the invoice again with invoice_id).
  • 404 No such quote of yours. not_found
  • 409 quote_invalidated: the connector that issued the quote no longer sells the product. Request a new quote.
  • 422 amount_out_of_range, reference_not_found, reference_rejected; 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/{id}/refresh" \
  -H "Authorization: Bearer $KULPAY_KEY"
201 Response application/json
{
  "amount": "250.00",
  "amount_minor": 25000,
  "biller": "EDM",
  "biller_name": "EDM",
  "breakdown": {
    "lines": [
      {
        "amount": "250.00",
        "concept": "Unit charge per kWh",
        "price": "10.42",
        "units": "24.00"
      }
    ],
    "unit_type": "kWh",
    "units": "24.00"
  },
  "created_at": "2026-10-10T22:14:58.651626029Z",
  "currency": "MZN",
  "customer_name": "C****** T**** 7***",
  "customer_reference": "01234567890",
  "fee_bearer": "payer",
  "fulfilment": {
    "attempts": 0,
    "status": "not_started"
  },
  "id": "3KWPl9Qy0bWm2ZkT4dNfE1sH7aP",
  "mode": "test",
  "object": "bill_payment",
  "participants": [
    {
      "account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
      "name": "Tenant B",
      "role": "initiating_psp"
    },
    {
      "account_id": "3KWPl6gHAomFsLp2Cbl41VxBTab",
      "name": "Tenant B",
      "role": "from_psp",
      "settlement": "prefunded_balance"
    }
  ],
  "payer_fee": "5.00",
  "payer_fee_minor": 500,
  "payment_status": "none",
  "product": "CREDELEC",
  "product_version": 1,
  "purchase_mode": "variable_amount",
  "quote_expires_at": "2026-10-10T22:24:58.651608397Z",
  "refreshed_from": "3KWPl4pk0XxRI4TEVI6avRgrj1y",
  "shape": "metered_prepaid",
  "status": "quoted",
  "supplier_connector": "edm-live",
  "total": "255.00",
  "total_minor": 25500
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "an invoice is quoted again with invoice_id",
  "error": {
    "code": "invalid_request",
    "message": "an invoice is quoted again with invoice_id"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such bill payment or quote",
  "error": {
    "code": "not_found",
    "message": "no such bill payment or quote"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · quote_invalidated application/problem+json
{
  "code": "quote_invalidated",
  "detail": "the supplier that issued this quote no longer sells Credelec; request a new quote",
  "error": {
    "code": "quote_invalidated",
    "message": "the supplier that issued this quote no longer sells Credelec; request a new quote"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}
422 Error · amount_out_of_range application/problem+json
{
  "code": "amount_out_of_range",
  "detail": "the amount must be between 20.00 MZN and 50000.00 MZN",
  "error": {
    "code": "amount_out_of_range",
    "message": "the amount must be between 20.00 MZN and 50000.00 MZN"
  },
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}
503 Error · biller_unavailable application/problem+json
{
  "code": "biller_unavailable",
  "detail": "the supplier is not answering; try again shortly",
  "error": {
    "code": "biller_unavailable",
    "message": "the supplier is not answering; try again shortly"
  },
  "status": 503,
  "title": "Service Unavailable",
  "type": "about:blank"
}

GET /v1/bill-payments/{id}

A bill payment

With its artifacts once succeeded. Requires bills:read.

Parameters

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": "anaexample.com",
  "via": "email"
}'
202 Response application/json
{
  "to": "***********.com",
  "via": "email"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "via is sms or email",
  "error": {
    "code": "invalid_request",
    "message": "via is sms or email"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such bill payment or quote",
  "error": {
    "code": "not_found",
    "message": "no such bill payment or quote"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
422 Error · resend_limit application/problem+json
{
  "code": "resend_limit",
  "detail": "the artifacts were already sent again 3 times",
  "error": {
    "code": "resend_limit",
    "message": "the artifacts were already sent again 3 times"
  },
  "status": 422,
  "title": "Unprocessable Content",
  "type": "about:blank"
}
503 Error · sms_unavailable application/problem+json
{
  "code": "sms_unavailable",
  "detail": "this gateway has no SMS channel configured; send by email",
  "error": {
    "code": "sms_unavailable",
    "message": "this gateway has no SMS channel configured; send by email"
  },
  "status": 503,
  "title": "Service Unavailable",
  "type": "about:blank"
}

GET /v1/biller/accounts/{ref}/qr

A customer account's QR (biller)

A QR for one of your customer accounts (BIL-5.13): print it once (on the contract, a bill, a sticker) and your customer pays every month. It encodes a plain https link any phone camera opens; the page shows what is due on the account now (its open invoices, oldest due first), each paid on its invoice page. A partner's app that reads it resolves it (POST /v1/qr/resolve) and pays in the app. In the mode of your key. Requires invoices:read.

Parameters

NameInTypeDescription
ref requiredpathstring The customer account or meter number: 1 to 40 printable characters, no spaces.
productquerystring The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it.

Responses

  • 200 The account's QR.
  • 400 invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active.
  • 404 No such biller. not_found

Request

curl -X GET "$KULPAY_URL/v1/biller/accounts/{ref}/qr" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "biller": "AGUAS",
  "mode": "live",
  "png_url": "/v1/biller/accounts/CL-001234/qr.png",
  "reference": "CL-001234",
  "svg_url": "/v1/biller/accounts/CL-001234/qr.svg",
  "url": "https://pay.kulpay.co.mz/pay/account/bB9BR1VBUx8fQ0wtMDA0Mg.3oT2Lw9sQe1Zk7mN"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "no product of AGUAS takes CL-****2 as a reference",
  "error": {
    "code": "invalid_request",
    "message": "no product of AGUAS takes CL-****2 as a reference"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/biller/accounts/{ref}/qr.png

A customer account's QR, PNG (biller)

A QR for one of your customer accounts (BIL-5.13): print it once (on the contract, a bill, a sticker) and your customer pays every month. It encodes a plain https link any phone camera opens; the page shows what is due on the account now (its open invoices, oldest due first), each paid on its invoice page. A partner's app that reads it resolves it (POST /v1/qr/resolve) and pays in the app. In the mode of your key. Requires invoices:read.

Parameters

NameInTypeDescription
ref requiredpathstring The customer account or meter number: 1 to 40 printable characters, no spaces.
productquerystring The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 400 invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active.
  • 404 No such biller. not_found

Request

curl -X GET "$KULPAY_URL/v1/biller/accounts/{ref}/qr.png" \
  -H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "no product of AGUAS takes CL-****2 as a reference",
  "error": {
    "code": "invalid_request",
    "message": "no product of AGUAS takes CL-****2 as a reference"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/biller/accounts/{ref}/qr.svg

A customer account's QR, rendered (biller)

A QR for one of your customer accounts (BIL-5.13): print it once (on the contract, a bill, a sticker) and your customer pays every month. It encodes a plain https link any phone camera opens; the page shows what is due on the account now (its open invoices, oldest due first), each paid on its invoice page. A partner's app that reads it resolves it (POST /v1/qr/resolve) and pays in the app. In the mode of your key. Requires invoices:read.

Parameters

NameInTypeDescription
ref requiredpathstring The customer account or meter number: 1 to 40 printable characters, no spaces.
productquerystring The product, when the biller has more than one that takes this reference (a meter: CREDELEC). The reference must be valid for it.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 400 invalid_request: the reference is not valid for the biller's products (or the product named), or the biller is not active.
  • 404 No such biller. not_found

Request

curl -X GET "$KULPAY_URL/v1/biller/accounts/{ref}/qr.svg" \
  -H "Authorization: Bearer $KULPAY_KEY"
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "no product of AGUAS takes CL-****2 as a reference",
  "error": {
    "code": "invalid_request",
    "message": "no product of AGUAS takes CL-****2 as a reference"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/biller/invoice-files

Upload an invoice file

A biller that presents by file sends a CSV (BIL-5.3) with the header invoice_number, account_reference, amount, currency, issue_date, due_date, description (issue_date and description optional), at most 10,000 rows and 5 MB. The file is checked as a whole, then loaded row by row as POST /v1/biller/invoices would; loading it again updates the unpaid invoices rather than duplicating them. Answers with the counts and a result per row, or the result file with ?format=csv. Records bill_invoice.file_loaded. Requires invoices:write.

Parameters

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
pay_urlstring The invoice's payment link, while it takes payments (BIL-5.9); reissue it with POST /v1/biller/invoices/{number}/payment-link.
payment_referencestring The ISO 11649 reference (RF…, grouped by four) a payer quotes to pay at a partner bank or PSP counter or by transfer (BIL-5.8).
pdf_urlstring (uri)
qrobject Its QR while it takes payments (BIL-5.12), on a read of one invoice: `url` is the invoice's payment link, which any phone camera opens; render it, or use the rendered images.
qr.png_urlstring
qr.svg_urlstring
qr.urlstring

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/invoices/{number}/qr.png

An invoice's QR as PNG (biller)

The QR to print on your invoice (BIL-5.12): it encodes the invoice's payment link (qr.url), a plain https link any phone camera opens; the page shows the invoice and takes the payment, and a partner's app that reads it pays it in the app. No personal data. Requires invoices:read.

Parameters

NameInTypeDescription
number requiredpathstring The biller's invoice number.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 404 No such invoice. not_found
  • 409 invoice_closed: a paid, cancelled or expired invoice has no QR.

Request

curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}/qr.png" \
  -H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
  "code": "invoice_closed",
  "detail": "invoice F-Q takes no payments, so it has no QR",
  "error": {
    "code": "invoice_closed",
    "message": "invoice F-Q takes no payments, so it has no QR"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/biller/invoices/{number}/qr.svg

An invoice's QR (biller)

The QR to print on your invoice (BIL-5.12): it encodes the invoice's payment link (qr.url), a plain https link any phone camera opens; the page shows the invoice and takes the payment, and a partner's app that reads it pays it in the app. No personal data. Requires invoices:read.

Parameters

NameInTypeDescription
number requiredpathstring The biller's invoice number.

Responses

  • 200 The QR: image/svg+xml for qr.svg, image/png for qr.png (512 px). Error correction M, with the quiet zone.
  • 404 No such invoice. not_found
  • 409 invoice_closed: a paid, cancelled or expired invoice has no QR.

Request

curl -X GET "$KULPAY_URL/v1/biller/invoices/{number}/qr.svg" \
  -H "Authorization: Bearer $KULPAY_KEY"
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
409 Error · invoice_closed application/problem+json
{
  "code": "invoice_closed",
  "detail": "invoice F-Q takes no payments, so it has no QR",
  "error": {
    "code": "invoice_closed",
    "message": "invoice F-Q takes no payments, so it has no QR"
  },
  "status": 409,
  "title": "Conflict",
  "type": "about:blank"
}

GET /v1/biller/payers

Your enrolled customers

Contacts masked. Requires payers:write.

Responses

  • 200 {data, total}.

Request

curl -X GET "$KULPAY_URL/v1/biller/payers" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "account_reference": "CL-10",
      "created_at": "2026-10-10T22:14:59.775211958Z",
      "lawful_basis": "contract",
      "phone_masked": "*****4567"
    },
    {
      "account_reference": "CL-9",
      "created_at": "2026-10-10T22:14:59.755120163Z",
      "lawful_basis": "contract",
      "phone_masked": "*****4567"
    }
  ],
  "total": 2
}

POST /v1/biller/payers

Enrol your customers

A biller registers its customers so they receive its bills on KulPay channels (BIL-5.5): lawful_basis (consent, contract, legal_obligation, legitimate_interest) and up to 1,000 payers with an account reference and a phone number or email. Each is matched to channels' payers by phone or email; matched payers are subscribed (their channel is told and can let them opt out), except those who opted out of that account. Answers per customer with payers_subscribed. Requires payers:write.

Request body application/json required

FieldTypeDescription
lawful_basis requiredstring
One of: consent, contract, legal_obligation, legitimate_interest
payers requiredobject[]
payers[].account_reference requiredstring
payers[].emailstring
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/products

Your products (biller)

Your products as KulPay sells them, with what you integrate against (BIL-5.15): supplier_product, the code your endpoints receive as product (the one you gave, else the product's own code; never empty), the reference each takes, whether lookups are routed to it, the fee you bear, and the connectors (endpoints) serving it. Requires invoices:read.

Responses

Request

curl -X GET "$KULPAY_URL/v1/biller/products" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "amount_rule": "range",
      "biller_fee": {
        "bearer": "biller",
        "currency": "MZN",
        "fixed": "5.00"
      },
      "code": "CONTA",
      "connectors": [
        {
          "driver": "biller_rest",
          "health": "healthy",
          "mode": "live",
          "name": "aguas-live"
        }
      ],
      "currency": "MZN",
      "customer_reference": {
        "example": "EX-123456",
        "label": "Contrato",
        "pattern": "^EX-\\d{6}$"
      },
      "enabled": true,
      "fee": {
        "bearer": "biller",
        "currency": "MZN"
      },
      "lookup_required": true,
      "max": "50000.00",
      "min": "1.00",
      "model": "agent",
      "name": "Conta de água",
      "purchase_modes": [
        "variable_amount"
      ],
      "routable": true,
      "shape": "postpaid",
      "supplier_product": "AGUA_DOM",
      "version": 2
    }
  ]
}
403 Error · not_a_biller application/problem+json
{
  "code": "not_a_biller",
  "detail": "this key is not a biller's",
  "error": {
    "code": "not_a_biller",
    "message": "this key is not a biller's"
  },
  "status": 403,
  "title": "Forbidden",
  "type": "about:blank"
}

GET /v1/biller/products/{product}/integration

A product's notification and report (biller)

The product's own integration with its LSP in a mode (BIL-5.16), secrets masked; defaults when none was set. Requires invoices:read.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).

Responses

  • 200 The integration.
  • 404 No such biller or product. not_found

Request

curl -X GET "$KULPAY_URL/v1/biller/products/{product}/integration" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "biller": "AGUAS",
  "fields": [
    "event",
    "notification_id",
    "payment_id",
    "request_id",
    "mode",
    "biller",
    "biller_name",
    "product",
    "supplier_product",
    "reference",
    "invoice_number",
    "amount",
    "payer_fee",
    "total",
    "currency",
    "amount_minor",
    "total_minor",
    "paid_at",
    "channel_id",
    "channel_name",
    "funding",
    "partner_reference",
    "supplier_reference",
    "receipt_url"
  ],
  "mode": "test",
  "notification": {
    "auth": "hmac",
    "content_type": "application/xml",
    "enabled": true,
    "headers": {
      "X-Biller-Code": "AGUAS-01"
    },
    "method": "POST",
    "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
    "url": "https://lsp.example/kulpay/pagamentos"
  },
  "product": "FACTURA",
  "report": {
    "auth": "bearer",
    "columns": [
      {
        "field": "paid_at",
        "header": "DATA",
        "width": 10
      },
      {
        "field": "invoice_number",
        "header": "FACTURA",
        "width": 12
      },
      {
        "align": "right",
        "field": "total",
        "header": "VALOR",
        "width": 10
      }
    ],
    "date_format": "DD/MM/YYYY",
    "decimal_separator": ",",
    "enabled": true,
    "file_name": "AGUAS_{date}.dat",
    "format": "txt",
    "header": true,
    "last_day": "2026-10-19",
    "line_ending": "lf",
    "url": "https://lsp.example/kulpay/reconciliacao"
  },
  "updated_at": "2026-10-20T08:00:00Z",
  "version": 3
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/biller/products/{product}/integration

Set a product's notification and report (biller)

Sets the payment notification (URL, body template, content type, auth, headers) and the daily report (format, columns, file name, URL) of a product in a mode (BIL-5.16). Fields left out keep their value. Each change is a new version (bill_product.integration_set). The change takes effect at once, in live too, with no second approval (owner decision 2026-10-20). Notification and report URLs follow the webhook URL rules: absolute https, no credentials, at most 500 characters, never a link-local, multicast or reserved address. Requires webhooks:manage.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).

Request body application/json required

FieldTypeDescription
notificationobject The payment notification: every payment of the product that succeeds is sent to `url`, its body rendered from `template` (Go text/template over the fields; helpers `json`, `xml`, `url` escape a value, `date "DD/MM/YYYY HH:mm"` formats paid_at, `cents` drops the decimal point). Checked when set: the body must be what `content_type` says. Retried for 24 h; kept for resends.
notification.authobject How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only.
notification.auth.passwordstring
notification.auth.secretstring
notification.auth.tokenstring
notification.auth.typestring
One of: none, bearer, basic, hmac
notification.auth.usernamestring
notification.content_typestring
One of: application/json, application/xml, application/x-www-form-urlencoded, text/plain
notification.enabledboolean
notification.headersobject Up to 10 static headers; not Authorization, Content-Type or KulPay-*.
notification.methodstring
One of: POST, PUT
notification.templatestring
notification.urlstring (uri)
reportobject The daily report: the product's payments of a day, in the LSP's format and file extension; downloadable, and sent to `url` the next day.
report.authobject How KulPay authenticates at the endpoint. `hmac` signs the body: KulPay-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "t.body">. Secrets are write-only.
report.auth.passwordstring
report.auth.secretstring
report.auth.tokenstring
report.auth.typestring
One of: none, bearer, basic, hmac
report.auth.usernamestring
report.columnsobject[]
report.columns[].alignstring
One of: left, right
report.columns[].field requiredstring
One of: event, notification_id, payment_id, request_id, mode, biller, biller_name, product, supplier_product, reference, invoice_number, amount, payer_fee, total, currency, amount_minor, total_minor, paid_at, channel_id, channel_name, funding, partner_reference, supplier_reference, receipt_url
report.columns[].headerstring
report.columns[].widthinteger txt (fixed width) only.
report.date_formatstring
report.decimal_separatorstring
One of: ., ,
report.enabledboolean
report.file_namestring Placeholders {biller}, {product}, {date}, {mode}; the extension is the one given, else the format's.
report.formatstring
One of: csv, tsv, psv, txt, json, xml, xlsx
report.headerboolean
report.line_endingstring
One of: lf, crlf
report.urlstring (uri)

Responses

Request

curl -X PUT "$KULPAY_URL/v1/biller/products/{product}/integration" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "notification": {
    "auth": {
      "secret": "<o seu segredo>",
      "type": "hmac"
    },
    "content_type": "application/xml",
    "enabled": true,
    "headers": {
      "X-Biller-Code": "AGUAS-01"
    },
    "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
    "url": "https://lsp.example/kulpay/pagamentos"
  },
  "report": {
    "auth": {
      "token": "<o seu token>",
      "type": "bearer"
    },
    "columns": [
      {
        "field": "paid_at",
        "header": "DATA",
        "width": 10
      },
      {
        "field": "invoice_number",
        "header": "FACTURA",
        "width": 12
      },
      {
        "align": "right",
        "field": "total",
        "header": "VALOR",
        "width": 10
      }
    ],
    "date_format": "DD/MM/YYYY",
    "decimal_separator": ",",
    "enabled": true,
    "file_name": "AGUAS_{date}.dat",
    "format": "txt",
    "url": "https://lsp.example/kulpay/reconciliacao"
  }
}'
200 Response application/json
{
  "biller": "AGUAS",
  "fields": [
    "event",
    "notification_id",
    "payment_id",
    "request_id",
    "mode",
    "biller",
    "biller_name",
    "product",
    "supplier_product",
    "reference",
    "invoice_number",
    "amount",
    "payer_fee",
    "total",
    "currency",
    "amount_minor",
    "total_minor",
    "paid_at",
    "channel_id",
    "channel_name",
    "funding",
    "partner_reference",
    "supplier_reference",
    "receipt_url"
  ],
  "mode": "test",
  "notification": {
    "auth": "hmac",
    "content_type": "application/xml",
    "enabled": true,
    "headers": {
      "X-Biller-Code": "AGUAS-01"
    },
    "method": "POST",
    "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>",
    "url": "https://lsp.example/kulpay/pagamentos"
  },
  "product": "FACTURA",
  "report": {
    "auth": "bearer",
    "columns": [
      {
        "field": "paid_at",
        "header": "DATA",
        "width": 10
      },
      {
        "field": "invoice_number",
        "header": "FACTURA",
        "width": 12
      },
      {
        "align": "right",
        "field": "total",
        "header": "VALOR",
        "width": 10
      }
    ],
    "date_format": "DD/MM/YYYY",
    "decimal_separator": ",",
    "enabled": true,
    "file_name": "AGUAS_{date}.dat",
    "format": "txt",
    "header": true,
    "last_day": "2026-10-19",
    "line_ending": "lf",
    "url": "https://lsp.example/kulpay/reconciliacao"
  },
  "updated_at": "2026-10-20T08:00:00Z",
  "version": 3
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "notification: the template does not render well-formed XML (use {{xml .field}} for values)",
  "error": {
    "code": "invalid_request",
    "message": "notification: the template does not render well-formed XML (use {{xml .field}} for values)"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/biller/products/{product}/integration/deliveries

A product's notifications and reports sent (biller)

The deliveries to the LSP, newest first: status pending (retrying), delivered or exhausted, attempts, the endpoint's last answer. Requires invoices:read.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).
kindquerystring
One of: notification, report

Responses

  • 200 The deliveries.
  • 404 No such biller or product. not_found

Request

curl -X GET "$KULPAY_URL/v1/biller/products/{product}/integration/deliveries" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "attempts": 1,
      "content_type": "application/xml",
      "created_at": "2026-10-20T08:00:00Z",
      "delivered_at": "2026-10-20T08:00:01Z",
      "duration_ms": 84,
      "id": "3KvxDelivery00000000000001",
      "integration_id": "3KvxIntegration0000000001",
      "kind": "notification",
      "method": "POST",
      "mode": "test",
      "next_attempt_at": "2026-10-20T08:00:00Z",
      "response_code": 200,
      "status": "delivered",
      "subject_id": "3KvxPayment000000000000001",
      "updated_at": "2026-10-20T08:00:01Z",
      "url": "https://lsp.example/kulpay/pagamentos"
    }
  ]
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/biller/products/{product}/integration/deliveries/{id}

One delivery and its exact body (biller)

What was sent, byte for byte: body (text) or body_base64 (xlsx). Requires invoices:read.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).
id requiredpathstring The delivery id.

Responses

  • 200 The delivery.
  • 404 No such biller or product. not_found

Request

curl -X GET "$KULPAY_URL/v1/biller/products/{product}/integration/deliveries/{id}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "body": "<pagamento><referencia>CL-1</referencia><valor>151000</valor><data>19/10/2026 12:00</data></pagamento>",
  "delivery": {
    "attempts": 1,
    "content_type": "application/xml",
    "created_at": "2026-10-20T08:00:00Z",
    "delivered_at": "2026-10-20T08:00:01Z",
    "duration_ms": 84,
    "id": "3KvxDelivery00000000000001",
    "integration_id": "3KvxIntegration0000000001",
    "kind": "notification",
    "method": "POST",
    "mode": "test",
    "next_attempt_at": "2026-10-20T08:00:00Z",
    "response_code": 200,
    "status": "delivered",
    "subject_id": "3KvxPayment000000000000001",
    "updated_at": "2026-10-20T08:00:01Z",
    "url": "https://lsp.example/kulpay/pagamentos"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/biller/products/{product}/integration/deliveries/{id}/retry

Send a delivery again (biller)

Sends a pending or exhausted delivery again now, the same body. Requires webhooks:manage.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).
id requiredpathstring The delivery id.

Responses

  • 200 The delivery after the attempt.
  • 404 No such biller or product. not_found

Request

curl -X POST "$KULPAY_URL/v1/biller/products/{product}/integration/deliveries/{id}/retry" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "body": "<pagamento>…</pagamento>",
  "delivery": {
    "attempts": 1,
    "content_type": "application/xml",
    "created_at": "2026-10-20T08:00:00Z",
    "delivered_at": "2026-10-20T08:00:01Z",
    "duration_ms": 84,
    "id": "3KvxDelivery00000000000001",
    "integration_id": "3KvxIntegration0000000001",
    "kind": "notification",
    "method": "POST",
    "mode": "test",
    "next_attempt_at": "2026-10-20T08:00:00Z",
    "response_code": 200,
    "status": "delivered",
    "subject_id": "3KvxPayment000000000000001",
    "updated_at": "2026-10-20T08:00:01Z",
    "url": "https://lsp.example/kulpay/pagamentos"
  }
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/biller/products/{product}/integration/preview

Preview a notification's body (biller)

Renders a template (the one given, else the product's) for a sample payment, as the LSP would receive it. Requires webhooks:manage.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).

Request body application/json required

FieldTypeDescription
content_typestring
templatestring

Responses

Request

curl -X POST "$KULPAY_URL/v1/biller/products/{product}/integration/preview" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "content_type": "application/xml",
  "template": "<pagamento><referencia>{{xml .reference}}</referencia><valor>{{cents .total}}</valor><data>{{date \"DD/MM/YYYY HH:mm\" .paid_at}}</data></pagamento>"
}'
200 Response application/json
{
  "body": "<pagamento><referencia>EX-123456</referencia><valor>126000</valor><data>19/10/2026 16:30</data></pagamento>",
  "content_type": "application/xml"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "template: unexpected \"}\" in operand",
  "error": {
    "code": "invalid_request",
    "message": "template: unexpected \"}\" in operand"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/biller/products/{product}/reports/{day}

A product's report for a day (biller)

The product's payments that succeeded on the day (Maputo), in its report format and file name (Content-Disposition). Requires invoices:read.

Parameters

NameInTypeDescription
product requiredpathstring The product code (KulPay's).
day requiredpathstring (date) The day, YYYY-MM-DD, Maputo time.

Responses

Request

curl -X GET "$KULPAY_URL/v1/biller/products/{product}/reports/{day}" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response text/plain
DATA      FACTURA          VALOR
19/10/2026F-N1           1510,00
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "the day is YYYY-MM-DD",
  "error": {
    "code": "invalid_request",
    "message": "the day is YYYY-MM-DD"
  },
  "status": 400,
  "title": "Invalid request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/biller/remittances

Your remittances

What KulPay remitted, or will remit, to you. Requires invoices:read.

Responses

  • 200 {data, total}.

Request

curl -X GET "$KULPAY_URL/v1/biller/remittances" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "count": 1,
      "created_at": "2026-10-10T22:14:59.538329669Z",
      "currency": "MZN",
      "fee_minor": 0,
      "gross_minor": 150000,
      "id": "3KWPlF7nZdWEWid2kjF1ZQJ718o",
      "mode": "test",
      "net_minor": 150000,
      "period_from": "2026-10-10T22:14:59.495269929Z",
      "period_to": "2026-10-10T22:14:59.495269929Z",
      "status": "pending"
    }
  ],
  "total": 1
}

GET /v1/biller/remittances/{id}/report.csv

Your remittance report

As the operators see it, for reconciling your customers' accounts. Requires invoices:read.

Parameters

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/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",
              "currency": "MZN",
              "name": "Pacote Básico",
              "price": "750.00",
              "price_minor": 75000,
              "validity": "30d"
            },
            {
              "code": "TVPREMIUM",
              "currency": "MZN",
              "name": "Pacote Premium",
              "price": "2500.00",
              "price_minor": 250000,
              "validity": "30d"
            }
          ],
          "shape": "account_topup"
        }
      ],
      "support_phone": "+258 21 480 500"
    }
  ],
  "total": 1
}

GET /v1/billers/{code}

One biller you may sell

As in the listing. Requires bills:read.

Parameters

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",
          "currency": "MZN",
          "name": "Pacote Básico",
          "price": "750.00",
          "price_minor": 75000,
          "validity": "30d"
        },
        {
          "code": "TVPREMIUM",
          "currency": "MZN",
          "name": "Pacote Premium",
          "price": "2500.00",
          "price_minor": 250000,
          "validity": "30d"
        }
      ],
      "shape": "account_topup"
    }
  ],
  "support_phone": "+258 21 480 500"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/billers/{code}/products/{product}/offers

A product's offers

The active offers of one product you may sell (IG-35), cheapest first, each with its decimal price, the exact price_minor and the product's currency, and the product's purchase_modes. Inactive offers are not listed. Responses carry an ETag and Cache-Control: max-age=300; send If-None-Match to revalidate. Requires bills:read.

Parameters

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

Responses

  • 200 {data, purchase_modes}.
  • 304 Not modified.
  • 404 No such biller, product or supply connector. not_found

Request

curl -X GET "$KULPAY_URL/v1/billers/{code}/products/{product}/offers" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "data": [
    {
      "code": "TVBASIC",
      "currency": "MZN",
      "name": "Pacote Básico",
      "price": "750.00",
      "price_minor": 75000,
      "validity": "30d"
    },
    {
      "code": "TVPREMIUM",
      "currency": "MZN",
      "name": "Pacote Premium",
      "price": "2500.00",
      "price_minor": 250000,
      "validity": "30d"
    }
  ],
  "purchase_modes": [
    "fixed_catalog",
    "variable_amount"
  ]
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such biller, product or supply connector",
  "error": {
    "code": "not_found",
    "message": "no such biller, product or supply connector"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/payers

Register or update a payer

Your payer in KulPay's payer directory (BIL-5.4), under your own external_id: 201 created, 200 updated. Name, phone and email are sealed and never put in events; phone and email are matched, as keyed hashes, to billers' enrolments, which subscribe the payer (bill_subscription.opted_in with origin: biller_enrolled), or, with enrolments: offer, offer the biller account to them (bill_subscription.offered, status offered): nothing of it is delivered until the payer accepts by subscribing with their consent, or declines by opting out. enrolments is kept when left out. Requires bills:write.

Request body application/json required

FieldTypeDescription
emailstring (email)
enrolmentsstring What a biller's matching enrolment does: subscribe (default; the payer can opt out) or offer (the payer is asked first).
One of: subscribe, offer
external_id 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.macuacuaexample.co.mz",
  "external_id": "CLI-000482",
  "id": "3K4mvOpRKqOU8KwHnmvdbsC0spb",
  "name": "Ana Macuácua",
  "phone": "258841234567"
}
201 Response application/json
{
  "created_at": "2026-10-10T22:14:59.658576008Z",
  "external_id": "u-1",
  "id": "3KWPlDfQKeSRExAGpYcLFhGY2jR",
  "name": "Ana",
  "phone": "258841234567"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "email is not an email address",
  "error": {
    "code": "invalid_request",
    "message": "email is not an email address"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}

GET /v1/payers/{id}

A payer

Requires bills:read.

Parameters

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.cossaexample.co.mz",
  "external_id": "cust-000184",
  "id": "3Kpx6EPL2FOQLXxXg0PeZMIUgIx",
  "name": "Joana Cossa",
  "phone": "+258841234567"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such payer, subscription or enrolment",
  "error": {
    "code": "not_found",
    "message": "no such payer, subscription or enrolment"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

GET /v1/payers/{id}/invoices

A payer's open bills

The open invoices of the billers the payer is subscribed to (source: presented, pay with invoice_id), and the amount due at lookup billers, asked now (source: lookup, pay with the lookup_id and product; unavailable when the biller does not answer) (BIL-5.6). New invoices arrive as bill_invoice.delivered webhooks, and bill_invoice.reminded three days before they are due. Requires bills:read.

Parameters

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. An account of a biller that pushes or files its invoices is active only when the biller's enrolment of that account names the payer's phone or email; otherwise it is pending_verification, with nothing delivered or listed, until such an enrolment arrives. Opting in again after opting out reactivates the subscription, and subscribing to an offered one accepts it. Records bill_subscription.opted_in. Requires bills:write.

Parameters

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), or declines an offered one: nothing of that biller account is delivered any more, and a later enrolment by the biller does not subscribe (nor offer) it to the payer again. Optional ?reason=. Records bill_subscription.opted_out. Requires bills:write.

Parameters

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

GET /v1/payers/{id}/subscriptions/{sid}/autopay

A payer's autopay

The subscription's mandate and its recent invoices (BIL-5.10). Requires bills:read.

Parameters

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

Responses

  • 200 The mandate.
  • 404 No such payer, subscription or mandate. not_found

Request

curl -X GET "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "consent_at": "2026-10-19T09:30:00Z",
  "consent_version": "autopay-terms-v1",
  "currency": "MZN",
  "days_before_due": 3,
  "funding": "collect",
  "id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
  "max_amount": "2000.00",
  "method": "mpesa",
  "mode": "live",
  "phone_masked": "25884****000",
  "runs": [
    {
      "amount": "1500.00",
      "attempts": 0,
      "currency": "MZN",
      "id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
      "invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
      "invoice_number": "FT-2026-0412",
      "next_attempt_at": "2026-11-07T00:00:00Z",
      "scheduled_for": "2026-11-07T00:00:00Z",
      "status": "scheduled"
    }
  ],
  "status": "active",
  "subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such payer, subscription or enrolment",
  "error": {
    "code": "not_found",
    "message": "no such payer, subscription or enrolment"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

PUT /v1/payers/{id}/subscriptions/{sid}/autopay

Set up a payer's autopay

Gives (or changes) the payer's mandate to pay the subscription's invoices automatically (BIL-5.10): funding balance (your balance; debit the payer's own account when told bill_autopay.succeeded) or collect (their wallet: collect.method mpesa, emola or mkesh, collect.phone, the payer's phone when left out); max_amount, the most one invoice is debited for; days_before_due (0 to 15); and the payer's consent to the autopay terms. Records bill_autopay.mandate_set. Each invoice delivered to the subscription, and each one open now, is scheduled for the later of its due date less days_before_due and now plus the pre-debit notice (bills.autopay_notice_hours, at least 24 hours in live mode), and you are told at once with bill_autopay.scheduled: notify the payer before the debit. An invoice above max_amount is not debited (bill_autopay.skipped, over_limit). A failed debit is tried again (bill_autopay.attempt_failed) up to bills.autopay_attempts times, then given up (bill_autopay.failed). The mandate runs in your key's mode. Requires bills:write.

Parameters

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

Request body application/json required

FieldTypeDescription
collectobject
collect.methodstring
One of: mpesa, emola, mkesh
collect.networkstring
collect.phonestring
consent requiredobject
consent.accepted requiredboolean
consent.text_version requiredstring
days_before_dueinteger
fundingstring
One of: balance, collect
max_amount requiredstring

Responses

  • 200 The mandate, active, with its scheduled invoices.
  • 400 Invalid funding, wallet, max_amount or days_before_due, or the subscription is not active. invalid_request
  • 404 No such payer or subscription. not_found
  • 422 consent_required: the payer's consent to the autopay terms.

Request

curl -X PUT "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "collect": {
    "method": "mpesa",
    "phone": "258841110000"
  },
  "consent": {
    "accepted": true,
    "text_version": "autopay-terms-v1"
  },
  "days_before_due": 3,
  "funding": "collect",
  "max_amount": "2000.00"
}'
200 Response application/json
{
  "consent_at": "2026-10-19T09:30:00Z",
  "consent_version": "autopay-terms-v1",
  "currency": "MZN",
  "days_before_due": 3,
  "funding": "collect",
  "id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
  "max_amount": "2000.00",
  "method": "mpesa",
  "mode": "live",
  "phone_masked": "25884****000",
  "runs": [
    {
      "amount": "1500.00",
      "attempts": 0,
      "currency": "MZN",
      "id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
      "invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
      "invoice_number": "FT-2026-0412",
      "next_attempt_at": "2026-11-07T00:00:00Z",
      "scheduled_for": "2026-11-07T00:00:00Z",
      "status": "scheduled"
    }
  ],
  "status": "active",
  "subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "max_amount, the most one invoice may be debited for, is a positive decimal",
  "error": {
    "code": "invalid_request",
    "message": "max_amount, the most one invoice may be debited for, is a positive decimal"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such payer, subscription or enrolment",
  "error": {
    "code": "not_found",
    "message": "no such payer, subscription or enrolment"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}
422 Error · consent_required application/problem+json
{
  "code": "consent_required",
  "detail": "the payer's consent to autopay is required: consent.accepted and the version of the terms they accepted",
  "error": {
    "code": "consent_required",
    "message": "the payer's consent to autopay is required: consent.accepted and the version of the terms they accepted"
  },
  "status": 422,
  "title": "Unprocessable Entity",
  "type": "about:blank"
}

DELETE /v1/payers/{id}/subscriptions/{sid}/autopay

Cancel a payer's autopay

Cancels the mandate at once (BIL-5.10): its scheduled invoices are not debited (bill_autopay.skipped, mandate_cancelled). Optional ?reason=. Records bill_autopay.mandate_cancelled; opting the payer out of the subscription cancels it too. Requires bills:write.

Parameters

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

Responses

  • 200 The mandate, cancelled.
  • 404 No such payer, subscription or mandate. not_found

Request

curl -X DELETE "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay" \
  -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
  "cancel_reason": "payer asked",
  "cancelled_at": "2026-10-20T08:00:00Z",
  "consent_at": "2026-10-19T09:30:00Z",
  "consent_version": "autopay-terms-v1",
  "currency": "MZN",
  "days_before_due": 3,
  "funding": "collect",
  "id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
  "max_amount": "2000.00",
  "method": "mpesa",
  "mode": "live",
  "phone_masked": "25884****000",
  "runs": [
    {
      "amount": "1500.00",
      "attempts": 0,
      "currency": "MZN",
      "id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
      "invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
      "invoice_number": "FT-2026-0412",
      "reason": "mandate_cancelled",
      "scheduled_for": "2026-11-07T00:00:00Z",
      "status": "skipped"
    }
  ],
  "status": "cancelled",
  "subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such payer, subscription or enrolment",
  "error": {
    "code": "not_found",
    "message": "no such payer, subscription or enrolment"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}

POST /v1/payers/{id}/subscriptions/{sid}/autopay/skip

Skip one invoice's autopay

The invoice is not debited (BIL-5.10): bill_autopay.skipped, payer_skipped. Only a scheduled invoice can be skipped. Requires bills:write.

Parameters

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

Request body application/json required

FieldTypeDescription
invoice_id requiredstring

Responses

  • 200 The mandate.
  • 400 The invoice is no longer scheduled. invalid_request
  • 404 No such payer, subscription, mandate or scheduled invoice. not_found

Request

curl -X POST "$KULPAY_URL/v1/payers/{id}/subscriptions/{sid}/autopay/skip" \
  -H "Authorization: Bearer $KULPAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8"
}'
200 Response application/json
{
  "consent_at": "2026-10-19T09:30:00Z",
  "consent_version": "autopay-terms-v1",
  "currency": "MZN",
  "days_before_due": 3,
  "funding": "collect",
  "id": "3KtqA2xYz9wV8uT7sR6qP5oN4mL",
  "max_amount": "2000.00",
  "method": "mpesa",
  "mode": "live",
  "phone_masked": "25884****000",
  "runs": [
    {
      "amount": "1500.00",
      "attempts": 0,
      "currency": "MZN",
      "id": "3KtqA1vRk0hS2bWfZ8yYc4mNpQe",
      "invoice_id": "3KtqA0aB2cD3eF4gH5iJ6kL7mN8",
      "invoice_number": "FT-2026-0412",
      "reason": "payer_skipped",
      "scheduled_for": "2026-11-07T00:00:00Z",
      "status": "skipped"
    }
  ],
  "status": "active",
  "subscription_id": "3Kt7ugR1VHfcfHLgVg7vXkohaqR"
}
400 Error · invalid_request application/problem+json
{
  "code": "invalid_request",
  "detail": "the invoice's autopay is paid and can no longer be skipped",
  "error": {
    "code": "invalid_request",
    "message": "the invoice's autopay is paid and can no longer be skipped"
  },
  "status": 400,
  "title": "Bad Request",
  "type": "about:blank"
}
404 Error · not_found application/problem+json
{
  "code": "not_found",
  "detail": "no such payer, subscription or enrolment",
  "error": {
    "code": "not_found",
    "message": "no such payer, subscription or enrolment"
  },
  "status": 404,
  "title": "Not Found",
  "type": "about:blank"
}