API reference
Routing
GET /v1/admin/launch-matrix
The launch capability matrix
The matrix the gateway enforces at its routing and authorization choke points (plan phase 10 item 9): operation x connector driver x method x mode x movement scope. A route it does not enable is refused before any reservation or provider call (route_not_enabled); off_net is refused outright (movement_scope_disabled). Payout rows are legacy behaviour kept pending the owner's decision. The default (2026-10-12.3) runs the simulated fleet (driver mock) in test mode only (owner decision 2026-10-12): a live charge, payout or name lookup that would run on it is route_not_enabled. It also lists cash deposits (cash_deposit, driver partner_api, method cash, scope acc_top): disabled in live mode, enabled in test mode (IG-52). Check production's connector bindings against this answer before every deploy. The binary runs the embedded default unless launch_matrix.file (KULPAY_LAUNCH_MATRIX_FILE) names another. Requires connectors:read.
Responses
- 200 The matrix.
Request
curl -X GET "$KULPAY_URL/v1/admin/launch-matrix" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"description": "Launch capability matrix ...",
"enforced": true,
"off_net": "refused",
"routes": [
{
"driver": "kulpay_balance",
"enabled": true,
"method": "kulpay_balance",
"mode": "live",
"operation": "charge",
"scope": "on_net"
},
{
"driver": "kulpay_balance",
"enabled": false,
"method": "kulpay_balance",
"mode": "live",
"operation": "payout",
"scope": "off_net"
}
],
"rows": [
{
"driver": "kulpay_balance",
"enabled": true,
"methods": [
"kulpay_balance"
],
"modes": [
"live",
"test"
],
"note": "KulPay Balance checkout with internal merchant settlement (phase 4B).",
"operation": "charge",
"scopes": [
"on_net"
]
},
{
"driver": "mpesa",
"enabled": true,
"legacy": true,
"methods": [
"mpesa"
],
"modes": [
"live",
"test"
],
"operation": "payout",
"owner_decision": "POST /v1/payouts ... pending the owner's phase 0 decision.",
"scopes": [
"legacy_payout"
]
}
],
"version": "2026-10-12.1"
}
GET /v1/admin/routing
Routing rules in force
The active rule set and its version, every version, and pending approvals (PSP-6.4). A PSP is eligible when its status allows the merchant (live; pilot for its allow-listed merchants; any working status in test mode), its circuit is closed, its capability matrix allows the request and its daily and monthly limits have room. With no matching rule a payment runs on its account's own connector binding. Failover (failover, max_failovers, default 2): after a soft decline or an error before the request was sent (connection refused, DNS failure, open circuit) the payment moves to the next eligible target, never on an unknown outcome or after the PSP may have accepted it; each move is transaction.failover.attempted and each attempt its own submit stage. Requires connectors:read.
Responses
- 200 The rule set.
Request
curl -X GET "$KULPAY_URL/v1/admin/routing" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"active": {
"rules": [
{
"id": "cards",
"match": {
"methods": [
"card"
]
},
"targets": [
{
"psp_id": "3KWPqtgPmeCaYplKVRxyJiriQOG",
"weight": 100
}
]
}
]
},
"active_version": 1,
"versions": [
{
"activated_at": "2026-10-10T22:15:44.945952516Z",
"approval_id": "3KWPqpGgl5DVi0nsBOTijTV7GWK",
"created_at": "2026-10-10T22:15:44.903403745Z",
"created_by": "key_accounts",
"id": "3KWPqrCkfWzVjEDju03YNzH5ntR",
"reason": "cards to Rota",
"status": "active",
"version": 1
}
]
}
POST /v1/admin/routing/dry-run
Dry-run routing rules on recent traffic
Replays the last days (default 7) of a mode's payments through rules (or stored version) and returns the expected split per PSP (weighted targets count by their share), the current split, hits per rule, payments that fall back to their own binding and those no PSP could take. Card BIN and phone-prefix conditions are not replayed (not stored with payments). Nothing is reserved or changed.
Request body application/json required
| Field | Type | Description |
|---|---|---|
days | integer | |
mode | string | One of: test, live |
rules | object[] | Ordered rules: the first that matches decides. |
rules[].description | string | |
rules[].failover | boolean | |
rules[].id required | string | |
rules[].match | object | Empty fields match anything. |
rules[].match.bin_ranges | object[] | |
rules[].match.countries | string[] | |
rules[].match.currencies | string[] | |
rules[].match.max_amount | string | |
rules[].match.merchants | string[] | Account ids or metadata.merchant_id values. |
rules[].match.methods | string[] | |
rules[].match.min_amount | string | |
rules[].match.phone_prefixes | string[] | |
rules[].match.types | string[] | |
rules[].max_failovers | integer | |
rules[].targets required | object[] | |
rules[].targets[].psp_id required | string | |
rules[].targets[].weight required | integer | Share among the eligible targets; 0 is a fallback only. |
version | integer |
Responses
- 200
{sample, by_psp: [{psp_id, name, expected, share, current}], by_rule, fallback, not_routed, truncated, limitation}.
Request
curl -X POST "$KULPAY_URL/v1/admin/routing/dry-run" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "test",
"rules": [
{
"id": "cards",
"match": {
"methods": [
"card"
]
},
"targets": [
{
"psp_id": "3KWPqtgPmeCaYplKVRxyJiriQOG",
"weight": 100
}
]
}
]
}'
200 Response application/json
{
"by_psp": [
{
"current": 0,
"expected": 3,
"name": "Rota Lda",
"psp_id": "3KWPqtgPmeCaYplKVRxyJiriQOG",
"share": 1
}
],
"by_rule": {
"cards": 3
},
"fallback": 0,
"limitation": "Card BIN and phone-prefix conditions are not replayed: payments do not store the card number or phone; such rules are counted as not matching.",
"not_routed": 0,
"sample": 3,
"truncated": false,
"window": "2026-10-03T22:15:44Z to 2026-10-10T22:15:44Z"
}
POST /v1/admin/routing/versions
Prepare a routing rules version
A new version of the whole rule set; it is active once a second person approves it (routing.activate, POST /v1/admin/approvals/{id}/decide). Requires connectors:write.
Request body application/json required
| Field | Type | Description |
|---|---|---|
reason required | string | |
rules required | object[] | Ordered rules: the first that matches decides. |
rules[].description | string | |
rules[].failover | boolean | |
rules[].id required | string | |
rules[].match | object | Empty fields match anything. |
rules[].match.bin_ranges | object[] | |
rules[].match.countries | string[] | |
rules[].match.currencies | string[] | |
rules[].match.max_amount | string | |
rules[].match.merchants | string[] | Account ids or metadata.merchant_id values. |
rules[].match.methods | string[] | |
rules[].match.min_amount | string | |
rules[].match.phone_prefixes | string[] | |
rules[].match.types | string[] | |
rules[].max_failovers | integer | |
rules[].targets required | object[] | |
rules[].targets[].psp_id required | string | |
rules[].targets[].weight required | integer | Share among the eligible targets; 0 is a fallback only. |
Responses
- 202 Prepared;
approvalis the request to approve. - 400 Invalid rules (each problem listed) or one already pending.
invalid_request
Request
curl -X POST "$KULPAY_URL/v1/admin/routing/versions" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "cards to Rota",
"rules": [
{
"id": "cards",
"match": {
"methods": [
"card"
]
},
"targets": [
{
"psp_id": "3KWPqtgPmeCaYplKVRxyJiriQOG",
"weight": 100
}
]
}
]
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-10T22:15:44.905300473Z",
"id": "3KWPqpGgl5DVi0nsBOTijTV7GWK",
"kind": "routing.activate",
"prepared_by": "key_accounts",
"reason": "routing rules v1: cards to Rota",
"status": "pending",
"subject_id": "3KWPqrCkfWzVjEDju03YNzH5ntR",
"tenant_id": "platform"
},
"message": "routing version prepared; a second person must approve it",
"version": {
"approval_id": "3KWPqpGgl5DVi0nsBOTijTV7GWK",
"created_at": "2026-10-10T22:15:44.903403745Z",
"created_by": "key_accounts",
"id": "3KWPqrCkfWzVjEDju03YNzH5ntR",
"reason": "cards to Rota",
"status": "pending",
"version": 1
}
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "at least one rule is required",
"error": {
"code": "invalid_request",
"message": "at least one rule is required"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/admin/routing/versions/{version}
One routing rules version
Parameters
| Name | In | Type | Description |
|---|---|---|---|
version required | path | integer |
Responses
- 200 The version and its rules.
- 404 No such version.
not_found
Request
curl -X GET "$KULPAY_URL/v1/admin/routing/versions/{version}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"rules": {
"rules": [
{
"id": "cards",
"match": {
"methods": [
"card"
]
},
"targets": [
{
"psp_id": "3KWPqtgPmeCaYplKVRxyJiriQOG",
"weight": 100
}
]
}
]
},
"version": {
"activated_at": "2026-10-10T22:15:44.945952516Z",
"approval_id": "3KWPqpGgl5DVi0nsBOTijTV7GWK",
"created_at": "2026-10-10T22:15:44.903403745Z",
"created_by": "key_accounts",
"id": "3KWPqrCkfWzVjEDju03YNzH5ntR",
"reason": "cards to Rota",
"status": "active",
"version": 1
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such routing version",
"error": {
"code": "not_found",
"message": "no such routing version"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}
POST /v1/admin/routing/versions/{version}/rollback
Roll back to a routing rules version
Prepares a copy of an earlier version, through the same approval.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
version required | path | integer |
Request body application/json required
| Field | Type | Description |
|---|---|---|
reason required | string |
Responses
- 202 Prepared.
- 404 No such version.
not_found
Request
curl -X POST "$KULPAY_URL/v1/admin/routing/versions/{version}/rollback" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Pagamento de energia, outubro"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-11T07:48:20Z",
"id": "3Kh8Qx4H3yHpjhVVX645vFcRXuU",
"kind": "routing.activate",
"prepared_by": "3KWTqydMTGykEIsWuGEP683gqgc",
"reason": "routing rules v8: Repor as regras da versão 5 após falhas no novo conector",
"status": "pending",
"subject_id": "3KymVIVtt0XsYONnValT0cGW9OX",
"tenant_id": "platform"
},
"version": {
"approval_id": "3Kh8Qx4H3yHpjhVVX645vFcRXuU",
"based_on": 5,
"created_at": "2026-10-11T07:48:20Z",
"created_by": "3KWTqydMTGykEIsWuGEP683gqgc",
"id": "3KymVIVtt0XsYONnValT0cGW9OX",
"reason": "Repor as regras da versão 5 após falhas no novo conector",
"status": "pending",
"version": 8
}
}
404 Error · not_found application/problem+json
{
"code": "not_found",
"detail": "no such routing version",
"error": {
"code": "not_found",
"message": "no such routing version"
},
"status": 404,
"title": "Not Found",
"type": "about:blank"
}