API reference
Routing
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.reference | object | The shape of a deposit reference (IO-1.1), on the reference upper-cased and without spaces: every condition set must hold. A lookup whose reference matches no rule goes to the default resolver. |
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/lookup-preview
Preview where deposit-reference lookups go
Routes sample deposit references through the active rule set and through a proposed one (IO-1.1), without calling any resolver or recording anything: for each, the rule and the resolver now and proposed, and whether it changes. Use it before proposing a rule set with reference conditions. Requires connectors:read.
Request body application/json required
| Field | Type | Description |
|---|---|---|
account_id | string | The partner whose lookups are previewed. |
method | string | |
mode | string | One of: test, live |
references required | string[] | |
rules required | object[] | The proposed rules, as in POST /v1/admin/routing/versions. |
Responses
- 200
{data, total}. - 400 The request body or query failed validation. The response body lists each field-level issue.
invalid_request
Request
curl -X POST "$KULPAY_URL/v1/admin/routing/lookup-preview" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "test",
"references": [
"01234567890",
"841234567"
],
"rules": [
{
"id": "contadores",
"match": {
"reference": {
"digits_only": true,
"max_length": 11,
"min_length": 11,
"prefixes": [
"0"
]
},
"types": [
"NAME_LOOKUP"
]
},
"targets": [
{
"psp_id": "psp_resolver",
"weight": 1
}
]
}
]
}'
200 Response application/json
{
"data": [
{
"changed": true,
"current": {
"psp_id": "psp_kuloffice",
"rule_id": "method_binding"
},
"normalized": "01234567890",
"proposed": {
"psp_id": "psp_resolver",
"rule_id": "contadores"
},
"reference": "01234567890"
}
],
"total": 1
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "The request body failed validation.",
"error": {
"code": "invalid_request",
"errors": [
{
"field": "money.amount",
"message": "must be a positive decimal with at most 2 places"
}
],
"message": "The request body failed validation."
},
"errors": [
{
"field": "money.amount",
"message": "must be a positive decimal with at most 2 places"
}
],
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
POST /v1/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.reference | object | The shape of a deposit reference (IO-1.1), on the reference upper-cased and without spaces: every condition set must hold. A lookup whose reference matches no rule goes to the default resolver. |
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"
}
KulPay