KulPayAPI Live

API reference

Reference

Reference data shared by every integration.

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

FieldTypeDescription
client_assertionstring
client_assertion_typestring
client_idstring
client_secretstring
grant_type requiredstring
One of: client_credentials
scopestring

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

NameInTypeDescription
provider requiredpathstring

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

NameInTypeDescription
provider requiredpathstring

Request body application/json required

FieldTypeDescription
entries requiredobject Raw code to KulPay code: the full set of overrides of this version.
reason requiredstring

Responses

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

NameInTypeDescription
includequerystring 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"
    }
  ]
}