API Live

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

FieldTypeDescription
daysinteger
modestring
One of: test, live
rulesobject[] Ordered rules: the first that matches decides.
rules[].descriptionstring
rules[].failoverboolean
rules[].id requiredstring
rules[].matchobject Empty fields match anything.
rules[].match.bin_rangesobject[]
rules[].match.countriesstring[]
rules[].match.currenciesstring[]
rules[].match.max_amountstring
rules[].match.merchantsstring[] Account ids or metadata.merchant_id values.
rules[].match.methodsstring[]
rules[].match.min_amountstring
rules[].match.phone_prefixesstring[]
rules[].match.typesstring[]
rules[].max_failoversinteger
rules[].targets requiredobject[]
rules[].targets[].psp_id requiredstring
rules[].targets[].weight requiredinteger Share among the eligible targets; 0 is a fallback only.
versioninteger

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

FieldTypeDescription
reason requiredstring
rules requiredobject[] Ordered rules: the first that matches decides.
rules[].descriptionstring
rules[].failoverboolean
rules[].id requiredstring
rules[].matchobject Empty fields match anything.
rules[].match.bin_rangesobject[]
rules[].match.countriesstring[]
rules[].match.currenciesstring[]
rules[].match.max_amountstring
rules[].match.merchantsstring[] Account ids or metadata.merchant_id values.
rules[].match.methodsstring[]
rules[].match.min_amountstring
rules[].match.phone_prefixesstring[]
rules[].match.typesstring[]
rules[].max_failoversinteger
rules[].targets requiredobject[]
rules[].targets[].psp_id requiredstring
rules[].targets[].weight requiredinteger Share among the eligible targets; 0 is a fallback only.

Responses

  • 202 Prepared; approval is 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

NameInTypeDescription
version requiredpathinteger

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

NameInTypeDescription
version requiredpathinteger

Request body application/json required

FieldTypeDescription
reason requiredstring

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