API reference
Reference
Reference data shared by every integration.
GET /.well-known/kulpay-apps
Where KulPay's apps live
The configured addresses of the docs, API reference, PSP onboarding, playground and (when the deployment names one) operator dashboard, so a static frontend can link to them. Unset addresses are omitted. No authentication.
Responses
- 200
{docs, api_reference, onboarding, playground, dashboard}, each optional.
Request
curl -X GET "$KULPAY_URL/.well-known/kulpay-apps" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"api_reference": "https://pay.example/docs/api",
"dashboard": "http://ops.internal/admin",
"docs": "https://pay.example/docs",
"lsps": "https://pay.example/lsp",
"onboarding": "https://pay.example/onboarding",
"onboarding_lsp": "https://pay.example/onboarding?kind=lsp",
"playground": "http://play.local",
"portal": "https://pay.example/portal",
"psps": "https://pay.example/psps"
}
GET /.well-known/kulpay-webhook-keys
Webhook verification keys
The region's Ed25519 public key for v1e= signatures (PSP-8.8). Public.
Responses
- 200
{keys: [{kid, alg: "Ed25519", region, public_key (base64)}]}.
Request
curl -X GET "$KULPAY_URL/.well-known/kulpay-webhook-keys" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"keys": [
{
"alg": "Ed25519",
"kid": "default-f6330aaf6000",
"public_key": "YX1rYJ/oAMOwV+j+Q6NAaTmVHL62qYkDsXJ2ajMwWgQ=",
"region": "default"
}
]
}
POST /oauth/token
OAuth 2.0 token (client credentials)
RFC 6749 §4.4 (PSP-8.7). The client authenticates with HTTP Basic or the form (client_id, client_secret), or a private-key JWT (RFC 7523: client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer, RS256 or ES256, iss=sub=client id, aud this endpoint's URL, exp within 5 minutes, single-use jti). The access token lives 15 minutes and carries the client's scopes, or the scope subset asked for; use it as a Bearer token. Errors follow RFC 6749 §5.2.
Request body application/x-www-form-urlencoded required
| Field | Type | Description |
|---|---|---|
client_assertion | string | |
client_assertion_type | string | |
client_id | string | |
client_secret | string | |
grant_type required | string | One of: client_credentials |
scope | string |
Responses
- 200
{access_token, token_type: "Bearer", expires_in: 900, scope}. - 400
invalid_request,invalid_scope,unsupported_grant_type. - 401
invalid_client.
Request
curl -X POST "$KULPAY_URL/oauth/token" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"access_token": "kpat_…",
"expires_in": 900,
"scope": "events:read keys:manage",
"token_type": "Bearer"
}
400 Error · invalid_scope application/json
{
"error": "invalid_scope",
"error_description": "the client may not request payouts:write"
}
401 Error · invalid_client application/json
{
"error": "invalid_client",
"error_description": "client authentication failed"
}
POST /secret-scanning/github
GitHub secret scanning partner endpoint
GitHub posts KulPay keys it finds in public (pattern kp_(live|test)_(sk|rk|pk)_[0-9A-Za-z]{49}), signed with ECDSA P-256 (Github-Public-Key-Identifier, Github-Public-Key-Signature over the raw body, keys from api.github.com/meta/public_keys/secret_scanning) (PSP-8.6). No API key. A genuine live key is revoked at once, its account emailed and credential.api_key.leaked recorded; a test key is reported, revoked only with secret_scanning.revoke_test_keys.
Request body application/json required
Responses
- 200
[{token_raw, token_type, label: true_positive|false_positive}]. - 401
invalid_signature.
Request
curl -X POST "$KULPAY_URL/secret-scanning/github" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"source": "string",
"token": "tok_7Hq2…",
"type": "standard",
"url": "https://merchant.example.co.mz/kulpay/return"
}
]'
200 Response application/json
[
{
"label": "true_positive",
"token_raw": "kp_live_sk_ZKy7C9e7AjvOQLo0b3lRbXJ7i2NbMErBEocvYshqpC34H2L23",
"token_type": "kulpay_api_key"
}
]
401 Error · invalid_signature application/problem+json
{
"code": "invalid_signature",
"detail": "the report's signature does not verify",
"error": {
"code": "invalid_signature",
"message": "the report's signature does not verify"
},
"status": 401,
"title": "Unauthorized",
"type": "about:blank"
}
GET /v1/admin/code-mappings
Provider code mappings
Providers with a table (built-in: mpesa, emola, iso8583; others normalise through KulPay's vocabulary), the codes seen without a mapping, and the families. Requires connectors:read.
Responses
- 200 Mappings.
Request
curl -X GET "$KULPAY_URL/v1/admin/code-mappings" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"data": [
{
"active_version": 0,
"entries": 5,
"provider": "emola"
},
{
"active_version": 0,
"entries": 26,
"provider": "iso8583"
}
],
"families": [
"approved",
"soft_decline"
],
"unmapped": []
}
GET /v1/admin/code-mappings/{provider}
A provider's code table
The effective table (built-in entries with the active version's on top), the versions and pending approvals.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
provider required | path | string |
Responses
- 200 The table.
Request
curl -X GET "$KULPAY_URL/v1/admin/code-mappings/{provider}" \
-H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"active_version": 0,
"entries": {
"INS-0": {
"code": "",
"description": "Request processed successfully",
"family": "approved"
},
"INS-1": {
"code": "processing_error",
"description": "Internal error",
"family": "error"
},
"INS-10": {
"code": "duplicate_transaction",
"description": "Duplicate transaction",
"family": "hard_decline"
},
"INS-13": {
"code": "connector_error",
"description": "Invalid shortcode used",
"family": "error"
},
"INS-14": {
"code": "connector_error",
"description": "Invalid reference used",
"family": "error"
},
"INS-15": {
"code": "amount_mismatch",
"description": "Invalid amount used",
"family": "hard_decline"
},
"INS-16": {
"code": "provider_unavailable",
"description": "Unable to handle the request due to a temporary overload",
"family": "error"
},
"INS-17": {
"code": "connector_error",
"description": "Invalid transaction reference length",
"family": "error"
},
"INS-18": {
"code": "connector_error",
"description": "Invalid transaction ID",
"family": "error"
},
"INS-19": {
"code": "connector_error",
"description": "Invalid third-party reference",
"family": "error"
},
"INS-2": {
"code": "connector_error",
"description": "Invalid API key",
"family": "error"
},
"INS-20": {
"code": "connector_error",
"description": "Not all parameters provided",
"family": "error"
},
"INS-2001": {
"code": "connector_error",
"description": "Initiator authentication error",
"family": "error"
},
"INS-2002": {
"code": "account_not_found",
"description": "Receiver invalid",
"family": "hard_decline"
},
"INS-2006": {
"code": "insufficient_funds",
"description": "Insufficient balance",
"family": "soft_decline"
},
"INS-2051": {
"code": "account_not_found",
"description": "MSISDN invalid",
"family": "hard_decline"
},
"INS-2057": {
"code": "connector_error",
"description": "Language code invalid",
"family": "error"
},
"INS-21": {
"code": "connector_error",
"description": "Parameter validations failed",
"family": "error"
},
"INS-22": {
"code": "connector_error",
"description": "Invalid operation type",
"family": "error"
},
"INS-23": {
"code": "",
"description": "Unknown status: query again",
"family": "unknown"
},
"INS-24": {
"code": "connector_error",
"description": "Invalid initiator identifier",
"family": "error"
},
"INS-25": {
"code": "connector_error",
"description": "Invalid security credential",
"family": "error"
},
"INS-26": {
"code": "connector_error",
"description": "Not authorized",
"family": "error"
},
"INS-4": {
"code": "transaction_not_allowed",
"description": "User is not active",
"family": "hard_decline"
},
"INS-5": {
"code": "user_cancelled",
"description": "Transaction cancelled by customer",
"family": "hard_decline"
},
"INS-6": {
"code": "generic_decline",
"description": "Transaction failed",
"family": "hard_decline"
},
"INS-9": {
"code": "",
"description": "Request timeout: result not yet known",
"family": "pending"
},
"INS-993": {
"code": "transaction_not_allowed",
"description": "Direct debit missing",
"family": "hard_decline"
},
"INS-994": {
"code": "duplicate_transaction",
"description": "Direct debit already exists",
"family": "hard_decline"
},
"INS-995": {
"code": "transaction_not_allowed",
"description": "Customer's profile has problems",
"family": "hard_decline"
},
"INS-996": {
"code": "transaction_not_allowed",
"description": "Customer account status not active",
"family": "hard_decline"
},
"INS-997": {
"code": "connector_error",
"description": "Linking transaction not found",
"family": "error"
},
"INS-998": {
"code": "transaction_not_allowed",
"description": "Invalid market",
"family": "hard_decline"
}
},
"pending_approvals": [
{
"created_at": "2026-10-10T22:15:03.509405079Z",
"id": "3KWPlkG2ypXCcvR1wlQ7iLTOoS6",
"kind": "code_mapping.activate",
"prepared_by": "key_accounts",
"reason": "mpesa codes v1: new code in production",
"status": "pending",
"subject_id": "3KWPlh0E8ChIlO54KGMxDffyHBC",
"tenant_id": "platform"
}
],
"provider": "mpesa",
"versions": [
{
"approval_id": "3KWPlkG2ypXCcvR1wlQ7iLTOoS6",
"created_at": "2026-10-10T22:15:03.507490707Z",
"created_by": "key_accounts",
"id": "3KWPlh0E8ChIlO54KGMxDffyHBC",
"provider": "mpesa",
"reason": "new code in production",
"status": "pending",
"version": 1
}
]
}
POST /v1/admin/code-mappings/{provider}
Propose a mapping version
A new version, active once a different person approves it (it decides payment outcomes): code_mapping.version_created, then code_mapping.activated. Requires connectors:write.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
provider required | path | string |
Request body application/json required
| Field | Type | Description |
|---|---|---|
entries required | object | Raw code to KulPay code: the full set of overrides of this version. |
reason required | string |
Responses
- 202 Pending approval.
- 400 An entry is not valid (
invalid_request).
Request
curl -X POST "$KULPAY_URL/v1/admin/code-mappings/{provider}" \
-H "Authorization: Bearer $KULPAY_KEY" \
-H "Content-Type: application/json" \
-d '{
"entries": {
"key": {
"code": "EDM",
"description": "Pagamento de energia, outubro",
"family": "approved"
}
},
"reason": "Pagamento de energia, outubro"
}'
202 Response application/json
{
"approval": {
"created_at": "2026-10-10T09:30:00Z",
"decided_at": "2026-10-10T09:30:00Z",
"decided_by": "string",
"decision_reason": "Pagamento de energia, outubro",
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"kind": "standard",
"prepared_by": "string",
"reason": "Pagamento de energia, outubro",
"status": "pending",
"subject_id": "3KUXQ43bztiuf9tCMNgbcrYvne3"
},
"mapping": {
"activated_at": "2026-10-10T09:30:00Z",
"approval_id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"created_at": "2026-10-10T09:30:00Z",
"created_by": "string",
"id": "3KUXQ43bztiuf9tCMNgbcrYvne3",
"provider": "mpesa",
"reason": "Pagamento de energia, outubro",
"status": "pending",
"version": 1
}
}
400 Error · invalid_request application/problem+json
{
"code": "invalid_request",
"detail": "INS-30: made_up is not a KulPay failure code",
"error": {
"code": "invalid_request",
"message": "INS-30: made_up is not a KulPay failure code"
},
"status": 400,
"title": "Bad Request",
"type": "about:blank"
}
GET /v1/transaction-types
Transaction type catalogue
Every transaction type with its money flow (debit and credit sides) and parent rules, and the currencies with their minor-unit exponents. Public reference data. Lists the supported types; ?include=reserved adds the names kept for later (TRANSFER, MANDATE, RECURRING, TOKENIZE, BALANCE_INQUIRY, FX_QUOTE), which KulPay does not process yet and no capability matrix may declare.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
include | query | string | reserved: list the reserved types too. One of: reserved |
Responses
- 200 The catalogue.
Request
curl -X GET "$KULPAY_URL/v1/transaction-types" \ -H "Authorization: Bearer $KULPAY_KEY"
200 Response application/json
{
"currencies": [
{
"code": "AED",
"exponent": 2,
"name": "UAE dirham"
},
{
"code": "AOA",
"exponent": 2,
"name": "Angolan kwanza"
}
],
"data": [
{
"amount_required": true,
"code": "SALE",
"credit": "merchant",
"debit": "customer",
"description": "Authorise and capture in one step: the customer pays the merchant.",
"holds_funds": false,
"moves_money": true,
"name": "Sale"
},
{
"amount_required": true,
"code": "AUTH",
"debit": "customer",
"description": "Reserve funds on the customer's account without moving them; a capture or void follows.",
"holds_funds": true,
"moves_money": false,
"name": "Authorisation"
}
]
}
KulPay