ULA API Gateway (v5.7.2)

Download OpenAPI specification:

Payment API Basics

This API is part of the our ecosystem. It allows you to make payments, find out the status of transactions and much more. Here you will find the latest documentation on setting up your solution.

Generating signature

Merchant’s request and callback have to be signed to verify sent data. To generate the signature all sent parameters from the payload are included in the order they were sent. The parameter signature should be excluded, of course,! and added to the payload after generating

Note: to generate a correct signature you need a secretKey received with other credentials.

PHP example

function calculateSignature(array $data, string $secretKey, string $currentParamPrefix = '', int $depth = 16, int $currentRecursionLevel = 0 ): string
{
    if ($currentRecursionLevel >= $depth) {
        throw new Exception('Recursion level exceeded');
    }

    $stringForSignature = '';
    foreach ($data as $key => $value) {
        if (is_array($value)) {
                $stringForSignature .= calculateSignature(
                $value,
                $secretKey,
                "$currentParamPrefix$key.",
                    $depth,
                $currentRecursionLevel + 1
            );
      } else if ($key !== 'signature') {
                $stringForSignature .= "$currentParamPrefix$key" . $value;
      }
   }

    if ($currentRecursionLevel == 0) {
      return strtolower(hash_hmac('sha512', $stringForSignature, $secretKey));
    } else {
      return $StringForSignature;
    }
 }

$postData = [
  'merchant_id' => 'fffed61be9780b97c5e4c65e4e07bb6b',
  'provider_id' => 10,
  'client_id' => '080000000',
  'country' => 'KE',
  'order_id' => 'order_3444298767545',
  'amount' => 1000,
  'currency' => 'CDF',
  'callback_url' => 'https://my.callback.url'
];

$secretKey = "cf11635572c1e8d77297207152dc0791ad91f22b32d23c758ce3ba2637202ad8f7290ba41f2243cccf32edde1dfb8bf0f5dea62525309e293b3adb2c76eed6a5";

$signature = calculateSignature($postData, $secretKey);

$postData['signature'] = $signature;

Examples in other languages are available on request

Status Codes

The parameters below will be obtained by a status query

Code Name Description
-1 undefined Operation status is undefined (for example in an error situation)
0 initiated Operation is initiated
1 in progress Operation is in progress
2 success Operation is successful
3 failed Operation is failed
4 cancelled Operation is cancelled

Operation Types

Depending on the type of request you may see the following code

You can see this parameter in the callback

Code Operation
16 payment_b2c
17 payment_c2b

Available currencies

Code Name Locations
KES (Example) Kenyan shilling the Republic of Kenya

Responses for confirmation requests have the same format as original operation responses.

Callbacks

C2b transaction status is sent via callback because it needs a confirmation by client done asynchronously. Usually the callback should be sent in 2-3 minutes maximum. In case of missing callback there is a way to get the transaction status using API method status. It needs the order ID as an parameter and returns a status of the performed transaction.

Response for callback

Payment gateway considers the Merchant system response as successful if HTTP 200 was received.

Payment Methods

Simulator

Provider ID Provider Name Notes
14 Simulator For testing purposes

During tests runs, using 14 provider ID (simulator) the callback is not returned and the transaction remains in the "in progress" status and if successful you will see in the response

{
  "order_id": "54321",
  "transaction_id": "12345",
  "transaction_ref": "",
  "status": 1,
  "result": {
      "code": 0,
      "message": "OK"
  },
  "provider_result": {
      "code": -8888,
      "message": "Good"
  },
  "service_id": 1,
  "service_version": "1.03/1.14|1.0/1.26|1.0/1.0|1.01/1.01|1.01/1.01||1.01/1.27",
  "service_date_time": "2023-05-15 10:00:00.000000",
  "confirm_type": 0
}

API Methods

Customer to the Business

path Parameters
public_id
required
string
Example: f54ec96649be11ebb3780242ac130002

Merchant public ID

Request Body schema: application/json
required

Parameters to initiate a customer to the merchant payment

merchant_id
required
string (merchantIdDef)

Unique Merchant ID received during the merchant registration

customer_id
required
string (customerIdDef)

Customer ID (usually mobile phone number of the customer)

order_id
required
string (orderIdDef)

The unique value is generated by the transaction initiator for each Operation. Max length is 128 symbols. Allowed symbols: [a-z], [A-Z], [0-9], “_” (underscore character), “-” (hyphen), “:” (colon), “.” (dot). For example, GUID or TIMESTAMP can be used as an order_id. This parameter provides API idempotency. It means that requests with identical nonce from the same transaction initiator will have identical responses and The corresponding operation won’t be repeated.

amount
required
string

Amount to pay, should be in format with two digits after point

currency
required
string (currencyDef)

Currency code in ISO 4217 format from the list of availabe currencies

country
string (countryDef)

Country code in ISO 3166-1 alpha-2 format as defined in the payment providers.

callback_url
string

URL to notify the merchant via callback. Recommended

provider_id
required
integer (providerDef)

Provider ID. Can be one of the option from this list.

object

Extra parameters

signature
required
string (signatureDef)

Merchant’s request and callback have to be signed to verify sent data. To generate the signature all sent parameters are included in the order they were sent. The parameter signature should be excluded, of course. Example can be found here

Responses

Callbacks

Request samples

Content type
application/json
{
  • "merchant_id": "e0fecd91fcb24f348048193b3fb34875ba3722b4",
  • "customer_id": "254900000001",
  • "order_id": "16280954971628095497",
  • "amount": "100.00",
  • "currency": "CDF",
  • "country": "KE",
  • "callback_url": "https://example.com/callback",
  • "provider_id": 14,
  • "extra": {
    },
  • "signature": "d7d6d76b0e22c6f9d369fa6c24f107053d12bfd24d3b154f2deb6676bf179c123134e1f20879c803be455d81cfe792f00cd8892c26ce7cf5a05beebb9c80843e"
}

Response samples

Content type
application/json
{
  • "order_id": "16280954971628095497",
  • "transaction_id": "732007046722",
  • "transaction_ref": "MP.33234.342.CP33",
  • "status": 1,
  • "result": {
    },
  • "provider_result": {
    },
  • "service_id": 1,
  • "service_version": 11.1,
  • "service_date_time": "2020-11-25 10:08:32.832969"
}

Callback payload samples

Callback
POST: Asynchronous notification of the merchant about the last performed transaction
Content type
application/json
{
  • "merchant_id": "e0fecd91fcb24f348048193b3fb34875ba3722b4",
  • "operation_type": 17,
  • "customer_id": "254900000001",
  • "amount": 100,
  • "order_id": "16280954971628095497",
  • "transaction_id": "1234567",
  • "transaction_ref": "QR555RQ",
  • "status": 2,
  • "provider_id": 14,
  • "destination_id": "",
  • "result": {
    },
  • "provider_result": {
    },
  • "service_id": 1,
  • "service_version": "1.03/1.0|1.0/1.26|1.0/1.0|1.01/1.0|1.01/1.0||1.01/1.27",
  • "service_date_time": "2020-11-25 10:08:32.832969",
  • "signature": "d7d6d76b0e22c6f9d369fa6c24f107053d12bfd24d3b154f2deb6676bf179c123134e1f20879c803be455d81cfe792f00cd8892c26ce7cf5a05beebb9c80843e"
}

Business to the Customer

path Parameters
public_id
required
string
Example: f54ec96649be11ebb3780242ac130002

Merchant public ID

Request Body schema: application/json
required

Parameters to initiate the merchant to the customer payment

merchant_id
required
string (merchantIdDef)

Unique Merchant ID received during the merchant registration

customer_id
required
string (customerIdDef)

Customer ID (usually mobile phone number of the customer)

order_id
required
string (orderIdDef)

The unique value is generated by the transaction initiator for each Operation. Max length is 128 symbols. Allowed symbols: [a-z], [A-Z], [0-9], “_” (underscore character), “-” (hyphen), “:” (colon), “.” (dot). For example, GUID or TIMESTAMP can be used as an order_id. This parameter provides API idempotency. It means that requests with identical nonce from the same transaction initiator will have identical responses and The corresponding operation won’t be repeated.

amount
required
string

Amount to pay, with two digits after point

currency
required
string (currencyDef)

Currency code in ISO 4217 format from the list of availabe currencies

country
string (countryDef)

Country code in ISO 3166-1 alpha-2 format as defined in the payment providers.

callback_url
string

URL to notify the merchant via callback

provider_id
required
integer (providerDef)

Provider ID. Can be one of the option from this list.

object

Extra parameters

signature
required
string (signatureDef)

Merchant’s request and callback have to be signed to verify sent data. To generate the signature all sent parameters are included in the order they were sent. The parameter signature should be excluded, of course. Example can be found here

Responses

Request samples

Content type
application/json
{
  • "merchant_id": "e0fecd91fcb24f348048193b3fb34875ba3722b4",
  • "customer_id": "254900000001",
  • "order_id": "16280954971628095497",
  • "amount": "100.00",
  • "currency": "CDF",
  • "country": "KE",
  • "callback_url": "https://example.com/callback",
  • "provider_id": 14,
  • "extra": {
    },
  • "signature": "d7d6d76b0e22c6f9d369fa6c24f107053d12bfd24d3b154f2deb6676bf179c123134e1f20879c803be455d81cfe792f00cd8892c26ce7cf5a05beebb9c80843e"
}

Response samples

Content type
application/json
{
  • "order_id": "16280954971628095497",
  • "transaction_id": "732007046722",
  • "transaction_ref": "MP.33234.342.CP33",
  • "status": 1,
  • "result": {
    },
  • "provider_result": {
    },
  • "service_id": 1,
  • "service_version": 11.1,
  • "service_date_time": "2020-11-25 10:08:32.832969"
}

Status check

path Parameters
public_id
required
string
Example: f54ec96649be11ebb3780242ac130002

Merchant public ID

Request Body schema: application/json
required

Get the status of the performed transaction

merchant_id
required
string (merchantIdDef)

Unique Merchant ID received during the merchant registration

order_id
required
string (orderIdDef)

The unique value is generated by the transaction initiator for each Operation. Max length is 128 symbols. Allowed symbols: [a-z], [A-Z], [0-9], “_” (underscore character), “-” (hyphen), “:” (colon), “.” (dot). For example, GUID or TIMESTAMP can be used as an order_id. This parameter provides API idempotency. It means that requests with identical nonce from the same transaction initiator will have identical responses and The corresponding operation won’t be repeated.

signature
required
string (signatureDef)

Merchant’s request and callback have to be signed to verify sent data. To generate the signature all sent parameters are included in the order they were sent. The parameter signature should be excluded, of course. Example can be found here

Responses

Request samples

Content type
application/json
{
  • "merchant_id": "e0fecd91fcb24f348048193b3fb34875ba3722b4",
  • "order_id": "16280954971628095497",
  • "signature": "d7d6d76b0e22c6f9d369fa6c24f107053d12bfd24d3b154f2deb6676bf179c123134e1f20879c803be455d81cfe792f00cd8892c26ce7cf5a05beebb9c80843e"
}

Response samples

Content type
application/json
{
  • "order_id": "16280954971628095497",
  • "transaction_id": "532007056722",
  • "transaction_ref": "",
  • "status": 1,
  • "result": {
    },
  • "provider_result": {
    },
  • "extra": {
    },
  • "service_id": 11,
  • "service_version": 11.1,
  • "service_date_time": "2020-11-25 10:08:32.832969",
  • "confirm_type": 0
}

Payment Page Guide

Quick start for redirect-based payments via the payment page


What is it?

A simple way to process payments by redirecting the customer to a hosted payment page. Your integration sends one INIT request, receives a pp_url link, redirects the customer, and waits for a callback — no multi-step API logic required.


How it works

INIT request → receive pp_url → redirect customer → customer pays → callback

Status codes

Status Meaning Action
8 Payment page URL ready Redirect customer to pp_url
1 In progress Wait for callback or poll STATUS
2 Success Done
3 Error Check result.message

Required parameters (INIT)

{
  "merchant_id": "string",
  "order_id": "string",
  "amount": "string",
  "currency": "string",
  "customer_id": "string",
  "customer_user_id": "string",
  "provider_id": "integer",
  "signature": "string"
}

Optional:

  • callback_url — URL to receive the final payment result

Response example

A successful INIT returns status: 8 and a pp_url to redirect the customer to.

{
  "order_id": "example_order_id_***",
  "transaction_id": "",
  "transaction_ref": "",
  "status": 8,
  "result": {
    "code": 0,
    "message": "request error"
  },
  "provider_result": {
    "code": -8888,
    "message": ""
  },
  "service_id": 1,
  "service_version": "1.03/1.14|1.0/2.0|1.0/1.0|1.01/1.0|1.01/2.0||1.02/1.27",
  "service_date_time": "2026-04-09 14:08:48.535688",
  "pp_url": "https://pp.domain.example/cdeffe6210c7fdc7402c13b210dbd8ddec02da0c/****",
  "confirm_type": 0
}

Important notes

✅ Redirect immediately — send the customer to pp_url as soon as you receive it

✅ Use callback_url — the final payment result arrives via callback, not in the INIT response

✅ Poll STATUS as fallback — if no callback arrives within a few minutes, use the STATUS endpoint to check the result


Common mistakes

❌ Expecting status: 2 directly in the INIT response — the page handles the completion asynchronously

❌ Not providing callback_url — you will miss the final payment result

❌ Sending customer data in extra — the payment page collects all required fields itself

Backoffice API overview

Separate functionality. This section documents the Backoffice API (bo-api.ulapay.co.mz) — a different API from the merchant Payment API documented above. It is not part of the payment_c2b / payment_b2c / status integration and uses its own base URL and authentication (OAuth 2.0 Client Credentials, not request signatures).

Payment API Backoffice API
Base URL https://api.ulapay.co.mz https://bo-api.ulapay.co.mz
Authentication Request signature (signature) OAuth 2.0 Client Credentials, Authorization: Bearer
Credentials Merchant ID and secret key Client ID and Client Secret from the Backoffice

1. Purpose

This feature gives Backoffice entities programmatic access to the BO API through the OAuth 2.0 Client Credentials Grant, decoupled from any interactive user session.


2. Process overview

The flow has three sequential stages:

  1. Generate credentials — create a client_id / client_secret pair bound to a specific entity, through the Backoffice UI (see Getting credentials).
  2. Get an access token — exchange the pair for a JSON Web Token (JWT) by calling /oauth/token (see Authentication).
  3. Make an authorized request — use the token in the Authorization header to call protected BO API endpoints.

The access token's lifetime is 1800 seconds (30 minutes). Once it expires, request a new one using the same credentials.

Getting credentials

3. Generating credentials

3.1 Steps

  1. Open My Profile in the Backoffice UI.
  2. In the Roles & Access block, select the card for the entity you need.
  3. In the Backoffice API Access section, click Generate API credentials.
  4. Save the Client ID and Client Secret shown in the dialog.

3.2 Important notes

  • The Client Secret is shown once, in plaintext, immediately after generation. It cannot be retrieved again in plaintext afterwards.
  • If a pair has already been generated for the entity, the control becomes Regenerate Secret (see section 4).

3.3 Assigning a role for API access

  • For the issued credentials to grant access to protected BO API endpoints, the entity must have at least one role flagged for API access (is_api_user). The roles available to the entity for API access are listed in the access token under api_user_roles (see section 5.4).
  • Role assignment is done by the partner's administrator. If no eligible role is assigned, requests to protected endpoints are rejected with 403 no_api_user_roles (see Errors).

4. Regenerating credentials

4.1 Steps

  1. On the entity's card, click Regenerate Secret.
  2. Confirm the action in the dialog.
  3. Save the new Client Secret shown in the dialog.

4.2 Important notes

  • The Client ID stays the same across regenerations.
  • The previous Client Secret is invalidated immediately once the action is confirmed.
  • Access tokens issued before the regeneration remain valid until they expire (expires_in) — regenerating does not immediately revoke tokens already issued.

Warning. Regenerating credentials for an entity used by a live integration will break that integration until the consumer updates its stored credentials.

Authentication

5. Getting an access token

5.1 Request

POST /api/v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}

The same parameters can be sent as application/json.

5.2 Example

curl -X POST 'https://bo-api.ulapay.co.mz/api/v1/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>'

5.3 Response

{
  "access_token": "string",
  "token_type": "Bearer",
  "expires_in": 1800
}

This grant does not issue a refresh token. Once the token expires (expires_in), repeat the request above with the same client_id and client_secret to get a new one. Re-requesting a token needs no additional confirmation and can be done as often as needed, within rate limits (429 when exceeded).

5.4 Token structure

The access token carries an entities claim — an array with a single element, corresponding to the entity the credentials were issued for:

Field Type Description
entity_type string Entity type
entity_id integer or null Entity identifier
roles array[string] Full list of roles assigned to the entity
api_user_roles array[string] Subset of roles eligible for API access

The token also carries an auth_method claim set to client_credentials, marking how the token was obtained.


6. Making authorized requests

6.1 Request

GET /api/v1/{endpoint}
Authorization: Bearer {ACCESS_TOKEN}

6.2 Example

curl -X GET 'https://bo-api.ulapay.co.mz/api/v1/merchant-accounts?clients[]=<CLIENT>' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

6.3 Scope

The scope of the request is resolved automatically from the entity carried in the token. There's no need to pass X-ENTITY-TYPE / X-ENTITY-ID headers.

Requesting data belonging to an entity other than the one in the token is rejected (see Errors).

Send requests only to https://bo-api.ulapay.co.mz: a token used on another partner's domain is rejected with 403 entity_domain_mismatch.

Get an access token

Exchanges a client_id / client_secret pair for a JWT access token (OAuth 2.0 Client Credentials Grant). The token is valid for expires_in seconds (1800). No refresh token is issued — request a new token with the same credentials once the current one expires.

Request Body schema:
required
grant_type
required
string
Value: "client_credentials"
client_id
required
string
client_secret
required
string

Responses

Request samples

Content type
No sample

Response samples

Content type
application/json
{
  • "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx.xxxxx",
  • "token_type": "Bearer",
  • "expires_in": 1800
}

Errors

7. Response code reference

Code Value When it occurs
200 — Request succeeded, within the token's scope
400 invalid_request POST /oauth/token: a required parameter is missing
400 unsupported_grant_type POST /oauth/token: grant_type is not client_credentials
400 — Invalid query parameter, or a merchant/branch outside the token's entity. The body carries message (and errors per parameter) instead of error
401 invalid_client POST /oauth/token: unknown Client ID or wrong Client Secret
401 unauthorized The access token is missing, invalid or has expired
403 no_api_user_roles The entity has no role eligible for API access
403 insufficient_role A role is present, but isn't among the roles allowed for the requested endpoint
403 out_of_entity_scope The request falls outside the scope of the entity carried in the token
403 entity_domain_mismatch The token belongs to another partner — use bo-api.ulapay.co.mz
403 unknown_host The request was sent to an unknown domain
429 — Too many requests — retry later
503 — Service temporarily unavailable — retry later

Examples:

{ "error": "out_of_entity_scope" }
{
  "message": "The currency field must be an integer.",
  "errors": { "currency": ["The currency field must be an integer."] }
}

8. Additional notes

Access control is enforced at independent levels: the request domain must belong to the token's partner, a role check (api_user_roles) against the endpoint's requirements, and an entity-scope check. Passing one check doesn't guarantee passing the others.

Merchant accounts

9. Merchant balances

9.1 Purpose

Unified endpoint for retrieving merchant balances. Replaces the five separate balance endpoints previously used per entity type. Carries no query logic of its own on the BO API side — the request is proxied to the Balance Service as-is.

9.2 Request

GET /api/v1/merchant-accounts
Authorization: Bearer {ACCESS_TOKEN}

Query parameters

Parameter Type Required Description
clients[] array[integer] no Merchant identifier (client). For merchant_brand and merchant_company entities, omitting this parameter automatically scopes the query to the entity's own client.
date string (YYYY-MM-DD or YYYY-MM-DD HH:MM:SS) no Balance snapshot date. Omitting it returns the current balance.
branch integer no Branch identifier.
currency integer no Currency numeric code (ISO 4217).
integration integer no Provider integration identifier.
provider_id integer no Provider ID.

9.3 Scope resolution

The scope of the query is determined by the entity type carried in the access token:

entity_type Scope resolution
merchant_brand Automatically scoped to its own client, matching entity_id
merchant_company Automatically resolved to a client_id + branch_id pair
wl_partner Automatically scoped to the whitelabel matching entity_id
wl_branch Automatically scoped to its own branch

9.4 Response

{
  "accounts": [
    {
      "country": "KEN",
      "country_name": "Kenya",
      "currency": 404,
      "public_id": "1011104044040000000000XXXXXXXXXXXXXXXXXX",
      "total_amount": "1500.00",
      "hold_amount": "200.00",
      "available_amount": "1300.00",
      "client": 3017,
      "merchant_name": "Demo Merchant",
      "branch": 223,
      "payment_method": 1,
      "payment_method_name": "M-Pesa",
      "provider_id": 9052,
      "provider_id_name": "9052-M-Pesa-KEN"
    }
  ]
}

The date field is present in the response root only when the corresponding query parameter was supplied. Field relationship: available_amount = total_amount − hold_amount.

Note. country carries the short country code; the full name is in the separate country_name field. provider_id carries the Provider ID (gate), with its name in provider_id_name; the naming format depends on the specific provider integration.

9.5 Example requests

No parameters (merchant_brand entity):

curl -X GET 'https://bo-api.ulapay.co.mz/api/v1/merchant-accounts' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

The query is automatically scoped to the entity's own client.

Explicit clients[] and date:

curl -X GET 'https://bo-api.ulapay.co.mz/api/v1/merchant-accounts?clients[]=1225&date=2026-09-10' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>'

Requesting another merchant's data (merchant_brand entity):

curl -X GET 'https://bo-api.ulapay.co.mz/api/v1/merchant-accounts?clients[]=3024' \
  -H 'Authorization: Bearer <ACCESS_TOKEN_FOR_A_DIFFERENT_MERCHANT>'

Response: 400 {"message": "clients do not match current merchant brand."}.

Get merchant account balances

Unified endpoint for retrieving merchant balances. Replaces the five entity-specific balance endpoints previously used for each entity type. Carries no balance logic of its own — the request is proxied to the Balance Service as-is.

The scope of the query is resolved automatically from the entity carried in the access token; see the scope resolution table for how each entity type is scoped.

Authorizations:
bearerAuth
query Parameters
clients[]
Array of integers
Example: clients[]=3017

Merchant identifiers (client). For merchant_brand and merchant_company tokens it can be omitted — the query is scoped to the entity's own merchant.

date
string
Example: date=2026-10-22

Balance snapshot date, YYYY-MM-DD or YYYY-MM-DD HH:MM:SS. Omit for the current balance.

branch
integer

Branch identifier.

currency
integer
Example: currency=404

Currency numeric code (ISO 4217), e.g. 404 for KES.

integration
integer

Provider integration identifier.

provider_id
integer

Provider ID.

Responses

Response samples

Content type
application/json
{
  • "date": "2026-10-22",
  • "accounts": [
    ]
}