cURL php javascript java

Balance & Reconciliation API

The Balance API provides a programmatic means to retrieve the balance and list all the transactions processed by your Wave Business Wallet. With this API you will be able to easily do automated daily reconciliation, setup balance alerts, issue refunds and more.

For real time notifications about the payments received by your account, you should not use this API, but Webhooks.

ENDPOINTS:

GET /v1/balance
GET /v1/transactions
POST /v1/transactions/:transaction_id/refund

Base URL

All the endpoint paths referenced in the API document are relative to a base URL, https://api.wave.com.

Authentication

Authenticating to the API is done via API keys. These keys are bound to a single business wallet and can only interact with it.

If you need to interact with more than one wallet in your network, then you will have to obtain one key per wallet.

Each request must be sent over HTTPS and contain an authorization header specifying the bearer scheme with the api key:

Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6

Note that the actual key is much longer but for documentation purposes most of the characters have been replaced by an ellipses.

Obtaining your API key

You can manage API keys in the developer's section in the Wave Business Portal. You can create, view, and revoke keys, and define which specific APIs each key has access to.

Dev Portal

When you create a new API key, you will only see the full key once. Make sure you copy it without missing any characters, since it will be masked afterwards for security reasons.

Create API Key

Wave doesn't know your full key, but if you contact API support we can identify them by the last 4 letters that you see displayed in the business portal.

Request signing

Request signing allows you to prove the integrity of your API requests. When enabled, each request must include a Wave-Signature header containing a timestamp and an HMAC-SHA256 signature of the request body.

Request signing is optional. When you create an API key with request signing enabled, a signing secret is generated and shown to you once. After that, all requests made with that API key must include a valid Wave-Signature header.

Enabling request signing

When creating a new API key in the Wave Business Portal, select the option to enable request signing. You will receive a signing secret in the format:

wave_sn_AKS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Signing Secret

Constructing the signature

To sign a request, you must:

  1. Get the current time as a Unix timestamp (integer seconds).
  2. Construct the payload by concatenating the timestamp with the raw request body (i.e. timestamp + body).
  3. Compute the HMAC-SHA256 of the payload using your signing secret as the key.
  4. Set the Wave-Signature header to t={timestamp},v1={signature}.

Example Wave-Signature header:

Wave-Signature: t=1639081943,v1=942119aedf9fa377844cf010785fe14ef8478c72af0b73d62ea3941335b526a8

The Wave-Signature header consists of a timestamp prefixed by t= and a signature prefixed by v1=, separated by a comma.

Signature generation examples

# Generate the signature and make a signed request
TIMESTAMP=$(date +%s)
BODY='{"amount": "1000", "currency": "XOF"}'
SIGNING_SECRET="wave_sn_AKS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

SIGNATURE=$(printf '%s%s' "$TIMESTAMP" "$BODY" \
  | openssl dgst -sha256 -hmac "$SIGNING_SECRET" \
  | sed 's/^.* //')

curl -X POST \
  -H "Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6" \
  -H "Wave-Signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
  -H "Content-Type: application/json" \
  -d "$BODY" \
  https://api.wave.com/v1/checkout/sessions
<?php
$api_key = "wave_sn_prod_YhUNb9d...i4bA6";
$signing_secret = "wave_sn_AKS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

$body = json_encode([
    "amount" => "1000",
    "currency" => "XOF",
]);

$timestamp = time();
$signature = hash_hmac("sha256", $timestamp . $body, $signing_secret);
$wave_signature = "t={$timestamp},v1={$signature}";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.wave.com/v1/checkout/sessions");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer {$api_key}",
    "Wave-Signature: {$wave_signature}",
    "Content-Type: application/json",
]);

$result = curl_exec($ch);
curl_close($ch);
?>
const crypto = require('crypto');
const axios = require('axios');

const apiKey = "wave_sn_prod_YhUNb9d...i4bA6";
const signingSecret = "wave_sn_AKS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";

const body = JSON.stringify({
  amount: "1000",
  currency: "XOF",
});

const timestamp = Math.floor(Date.now() / 1000);
const payload = timestamp + body;
const signature = crypto.createHmac('sha256', signingSecret).update(payload).digest('hex');
const waveSignature = `t=${timestamp},v1=${signature}`;

axios.post("https://api.wave.com/v1/checkout/sessions", body, {
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Wave-Signature": waveSignature,
    "Content-Type": "application/json",
  },
});
import java.nio.charset.StandardCharsets;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class WaveRequestSigner {
    private static final byte[] HEX_ARRAY = "0123456789abcdef".getBytes(StandardCharsets.US_ASCII);

    public static String sign(String body, String signingSecret) throws Exception {
        long timestamp = System.currentTimeMillis() / 1000;
        String payload = timestamp + body;

        Mac sha256Hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec secretKey = new SecretKeySpec(
            signingSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        sha256Hmac.init(secretKey);
        byte[] hash = sha256Hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));

        String signature = bytesToHex(hash);
        return "t=" + timestamp + ",v1=" + signature;
    }

    private static String bytesToHex(byte[] bytes) {
        byte[] hexChars = new byte[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars, StandardCharsets.UTF_8);
    }
}

Timestamp validation

Wave validates that the signature timestamp is recent to prevent replay attacks. Requests are rejected if:

Ensure your server's clock is reasonably synchronized.

Signed request for GET endpoints

For GET requests that have no body, use an empty string as the body when constructing the signature payload. That is, the payload is just the timestamp string.

Error codes

In the authentication phase, one of the following errors can cause your request to return early:

Status Code Descriptions
401 missing-auth-header Your request should include an HTTP auth header.
401 invalid-auth Your HTTP auth header can't be processed.
401 api-key-not-provided Your request should include an API key.
401 no-matching-api-key The key you provided doesn't exist in our system.
401 api-key-revoked Your API key has been revoked.
401 missing-signature Your API key has request signing enabled but the Wave-Signature header is missing.
401 invalid-signature-format The Wave-Signature header is malformed. It must be in the format t={timestamp},v1={signature}.
401 invalid-signature The signature does not match. Verify you are using the correct signing secret and signing the raw request body.
401 invalid-signature-timestamp The timestamp in the signature is not a valid number.
401 expired-signature-timestamp The signature timestamp is too far from the current time.
403 invalid-wallet Your wallet can't be used in this particular API.
403 disabled-wallet Your wallet has been temporarily disabled. You should still be able to use the endpoint to check your balance.

IP Whitelisting

IP whitelisting restricts API access to requests originating from specific, pre-approved IP addresses. Even if an API key is compromised, requests from unrecognized IPs are rejected; adding a second layer of defense on top of key-based authentication.

This helps meet financial-services compliance requirements and gives internal teams tighter control over which servers can reach Wave's APIs.

How It Works

IP whitelisting works alongside API key authentication. When a request hits the API, three checks happen in order:

  1. API key validation: Is the API key valid?
  2. Enforcement check: Is IP whitelisting enabled for this wallet?
  3. IP validation: Does the request's source IP match an entry in the whitelist?

All three must pass for the request to succeed.

Configuration

IP whitelisting is managed in the Wave Business Portal under the Developer section.

Supported Formats

You can whitelist a single IP or a CIDR range:

Format Example Notes
Single IP 203.0.113.45 Stored as /32 (IPv4) or /128 (IPv6)
CIDR range 203.0.113.0/24 Whitelists an entire subnet

Validation Rules

The system enforces the following constraints:

Rule Detail
Minimum prefix length /8 for IPv4 (~16M addresses), /48 for IPv6 (standard site allocation)
No private/reserved IPs Private, reserved, loopback and link-local addresses are rejected
No overlapping ranges New entries must not overlap with existing whitelist entries
Normalization All entries are normalized to strict CIDR notation

Managing Entries

The portal lets you view all active whitelist entries, including labels and who created each one (created_by). Each entry should have a clear label describing the server, environment or purpose. This makes future audits much easier.

IP Whitelist Management

Error Handling

Requests from non-whitelisted IPs receive a 403 response:

Status Code Description
403 ip-not-allowed The request originated from an IP address not on the allowlist
{
  "error": {
    "code": "ip-not-allowed",
    "message": "Request from IP address not in allowlist",
    "httpcode": 403
  }
}

Best Practices

Use static IPs. Dynamic IPs that rotate will break your integration. In cloud environments (AWS, GCP), use elastic or reserved IPs.

Keep ranges narrow. Whitelist only the specific IPs that need access. A single /32 per server is ideal; resort to broader ranges only when your infrastructure requires it.

Label everything. Include the server name, environment (staging/production) or purpose. Six months from now, an unlabelled entry is an entry nobody wants to touch.

Review regularly. Decommissioned servers and old test environments accumulate. Periodically audit the whitelist and remove stale entries.

Types

Some of the parameters in Wave API requests or attributes in API objects follow specific data formats.

Amounts

All amounts are represented as strings. When the amount includes decimal places, the separator is a period (.). The following rules apply to valid amounts:

Currency

Standard ISO 4217 three-letter codes in upper case are used to specify currency. The code for the West African Franc is XOF. Decimal places are not allowed for XOF currency amounts.

Timestamps

Timestamp values follow the ISO 8601 standard. Timestamps that we provide in API objects will be of the form YYYY-MM-DDThh:mm:ssZ where the Z indicates the UTC time zone.

Transaction Type

Name Description
merchant_payment A payment from a customer to a business.
merchant_payment_refund A refund of a merchant payment.
api_checkout A payment from a customer to a business made through the Checkout API
api_checkout_refund A refund of a checkout payment.
api_payout A payment from a business to a customer, made through the Payout API
api_payout_reversal A reversal of a business payment.
bulk_payment A payment from a business to a customer, made through the Bulk Payments portal.
bulk_payment_reversal A reversal of a business payment.
b2b_payment A payment from a business to another business.
b2b_payment_reversal A reversal of payment from a business to another business.
merchant_sweep A reversal of payment from a business to another business.

The Balance object

BALANCE OBJECT

{
  "amount": "10245",
  "currency": "XOF"
}

The Balance object represents the amount of money in a Wave wallet.

Attributes


amount string
Balance amount.

currency currency
Three-letter ISO 4217 currency code.

API

Retrieve balance

GET /v1/balance

Retrieves the current balance of the account associated with the API key used to make the request.

curl \
-H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6' \
https://api.wave.com/v1/balance
$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://api.wave.com/v1/balance",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "GET",
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6"
  ],
]);

$response = curl_exec($curl);

curl_close($curl);
echo $response;
const axios = require('axios');

axios.get('https://api.wave.com/v1/balance', {
  headers: {
    'Authorization': 'Bearer wave_sn_prod_YhUNb9d...i4bA6'
  }
})
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.error(error);
});
HttpResponse<String> response = Unirest.get("https://api.wave.com/v1/balance")
.header("Authorization", "Bearer wave_sn_prod_YhUNb9d...i4bA6")
.asString();

System.out.println(response.getBody());

Parameters


URL parameters are passed in the form of https://api.wave.com/v1/balance?include_subaccounts=true.

Name Type Description
include_subaccounts URL parameter (optional) If your account has readable subaccounts (like an HQ account with supervisors or cashiers under them, then include their balance in the total.

Returns


Returns a balance object for the account associated with the API key used to make the request.

Retrieve your transactions

GET /v1/transactions

Returns a list of transactions of your account for a given day. Uses pagination if the number of transactions exceeds 1000 entries. The transactions are ordered from older to newer.

curl -X GET \
  --url https://api.wave.com/v1/transactions?date=2022-10-25 \
  -H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'
$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://api.wave.com/v1/transactions?date=2022-10-25",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 0,
  CURLOPT_FOLLOWLOCATION => true,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "GET",
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6"
  ],
]);

$response = curl_exec($curl);

curl_close($curl);
echo $response;
const axios = require('axios');

axios.get('https://api.wave.com/v1/transactions?date=2022-10-25', {
  headers: {
    'Authorization': 'Bearer wave_sn_prod_YhUNb9d...i4bA6'
  }
})
.then(response => {
  console.log(response.data);
})
.catch(error => {
  console.error(error);
});
HttpResponse<String> response = Unirest.get("https://api.wave.com/v1/transactions?date=2022-10-25")
.header("Authorization", "Bearer wave_sn_prod_YhUNb9d...i4bA6")
.asString();
System.out.println(response.getBody());

Example response

{
  "page_info": {
    "start_cursor": null,
    "end_cursor": "TFRfdUZ1MGoyMzVKemtz",
    "has_next_page": true
  },
  "date": "2022-11-07",
  "items": [
    {
      "timestamp": "2022-11-07T14:41:15Z",
      "transaction_id": "T_V3TFOUE7VU",
      "amount": "99",
      "fee": "1",
      "currency": "XOF",
      "counterparty_name": "Mame Diop",
      "counterparty_mobile": "+221761110000"
    },
    {
      "timestamp": "2022-11-07T14:42:06Z",
      "transaction_id": "T_2YJNPWMCIY",
      "amount": "99",
      "fee": "1",
      "currency": "XOF",
      "counterparty_name": "Fatou Ndiaye",
      "counterparty_mobile": "+221761110001"
    },
    {
      "timestamp": "2022-11-07T14:42:41Z",
      "transaction_id": "T_2YJNPWMCIY",
      "amount": "-99",
      "fee": "-1",
      "currency": "XOF",
      "is_reversal": true
    },
    {
      "timestamp": "2022-11-07T14:42:54Z",
      "transaction_id": "pt-1azcvz4081002",
      "amount": "-101",
      "fee": "1",
      "currency": "XOF",
      "counterparty_name": "Moustapha Mbaye",
      "counterparty_mobile": "+221761110519"
    },
    {
      "timestamp": "2022-11-07T14:43:00Z",
      "transaction_id": "pt-1azcw0qkg1004",
      "amount": "-121",
      "fee": "1",
      "currency": "XOF",
      "counterparty_name": "Moustapha Mbaye",
      "counterparty_mobile": "+221761110519"
    },
    {
      "timestamp": "2022-11-07T14:43:14Z",
      "transaction_id": "pt-1azcw0qkg1004",
      "amount": "121",
      "fee": "1",
      "currency": "XOF",
      "counterparty_name": "Moustapha Mbaye",
      "counterparty_mobile": "+221761110519",
      "is_reversal": true
    }
  ]
}

Request parameters

URL parameters are passed in the form of https://api.wave.com/v1/transactions?date=value1&after=value2.

Name Type Description
date URL parameter (optional) Which day to fetch transactions for. Returns the current day's transactions if not specified.
after URL parameter (optional) The pointer of the page to fetch. This is an opaque string, and generally you just pass it the end_cursor of the previous page, as returned in the page_info object. See pagination.
include_subaccounts URL parameter (optional) If your account has readable subaccounts (like an HQ account with supervisors or cashiers under them, then include their transactions as well. Defaults to False. Note: business reports in the business portal include all subaccount transactions by default.

Return attributes

Key Type Description
timestamp Timestamp The execution time and date of the transaction.
transaction_id String A unique identifier for a transaction. Up to 20 characters.
transaction_type Transaction Type The type of transaction.. Can be empty.
amount String The amount difference that this transaction had on your account.
fee String The fee for the transaction.
balance String The wallet's balance after this transaction was executed.
currency Currency code The 3-letter ISO 4217 currency code of the transaction amount.
is_reversal Boolean (optional) Marked true if this is a reversal or refund of a previous transaction.
counterparty_name String (optional) The name of the counterparty (sender or receiver) of the transaction.
counterparty_mobile String (optional) The mobile number of the counterparty (sender or receiver) of the transaction.
counterparty_id String (optional) The identifier of the counterparty in B2B payments.
business_user_name String (optional) The name of the business user involved in the transaction. This is for example the shop assistant/business user, or the depositor for an agent transaction.
business_user_mobile String (optional) The mobile number of the business user involved in the transaction.
employee_id String (optional) The employee ID associated with the busienss user involved in the transaction.
client_reference String (optional) A unique string that you (optionally) provided when submitting the transaction.
payment_reason String (optional) A payment reason or message that you (optionally) provided when submitting the transaction.
checkout_api_session_id String (optional) If this payment is linked to a Checkout API session, this is its ID.
batch_id String (optional) The batch ID if this transaction is part of a bulk payment.
aggregated_merchant_id String (optional) The ID of the aggregated merchant this transaction is assigned to. Only applicable if you're an aggregator processing payments for a merchant that is integrating via your services.
aggregated_merchant_name String (optional) The name of the aggregated merchant this transaction is assigned to. Only applicable if you're an aggregator processing payments for a merchant that is integrating via your services.
custom_fields JSON Object (optional) A key-value map of any custom fields related to this payment.
submerchant_id String (optional) If the transaction belongs to a sub-account, this field denotes the ID of the account that the transaction happened under.
submerchant_name String (optional) If the transaction belongs to a sub-account, this field denotes the name of the account that the transaction happened under.
government_tax_amount String (optional) The amount of government tax applied to this transaction (Senegal only).
government_tax_paid_by_wave String (optional) The amount of government tax covered by Wave on behalf of the merchant (Senegal only).

Refund a transaction

POST /v1/transactions/:transaction_id/refund

With this endpoint you can reverse a payment received, including fees. You don't need to indicate a reason, you can simply refund your customers any time you deem necessary.

curl -X POST \
  -L https://api.wave.com/v1/transactions/T_VZSWJF5MMQ/refund \
  -H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'

Example response

HTTP 200 OK

Request parameters

There are no parameters necessary in the request body. Note that the transaction_id is passed in the request path.

Response body

There is no response body. The status code will inform you whether a refund was successful (200), or the transaction doesn't exist (404), or something else went wrong (500).

Pagination

Many of Wave's listing endpoints support pagination. If you are using a paginated endpoint, you must be prepared to accept paginated responses and continue making requests until no items remain or you have retrieved all of the data your application needs.

Paginated request

Paginated endpoints currently only support forward pagination. Backward pagination is not yet supported.

Example paginated request

curl -X GET \
  --url 'https://api.wave.com/v1/paginated-endpoint?first=10&after=YzE6YW0tMWFrbjhkeWcwMTAwOA'

For pagination, you may use the following query parameters:

Parameter Type Description
first Integer (optional) The maximum number of items to retrieve. The server may respond with fewer items if your limit is too high, or if there aren't enough items remaining to fulfill the request.
after String (optional) An opaque cursor indicating where pagination should continue from. Typically you will use the value of page_info.end_cursor from the previous response.

Paginated response

Example paginated response

{
    "page_info": {
        "has_next_page": true,
        "end_cursor": "YzE6YW0tMWFrbjhkeWcwMTAwOA"
    },
    "items": [
        {
            "id": "xyz-bm29dahzl",
            "name": "Item 1"
        }
    ]
}

A paginated response contains pagination information and a list of items. The specific item type is endpoint-dependent.

Key Type Description
page_info Page Info Pagination information.
items List of objects A list of items. Item type varies by endpoint.

Page Info object

The page info object contains information related to the current page of results, and a cursors that can be used to retrieve the next page.

Key Type Description
has_next_page Boolean true if there is a next page after this one, false otherwise.
end_cursor String A cursor that can be used to fetch the next page of items.

Example pagination request flow

Example initial request

curl -X GET \
  --url 'https://api.wave.com/v1/paginated-endpoint?first=10' \
  -H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'

Example initial response

{
    "page_info": {
        "has_next_page": true,
        "end_cursor": "YzE6YW0tMWFrbjhkeWcwMTAwOA"
    },
    "items": [
      ...
    ]
}

As an example, we can retrieve items from an endpoint, ten items at a time.

Note: The first query parameter is optional if we don't care about the precise number of items being retrieved. The server will choose a sensible default for the collection being retrieved.

Example next-page request

curl -X GET \
  --url 'https://api.wave.com/v1/paginated-endpoint?first=10&after=YzE6YW0tMWFrbjhkeWcwMTAwOA' \
  -H 'Authorization: Bearer wave_sn_prod_YhUNb9d...i4bA6'

Example next-page response

{
    "page_info": {
        "has_next_page": false,
        "end_cursor": "YzE6YW0tMWFrbjhkeWc6iNNoqP"
    },
    "items": [
      ...
    ]
}

When the response's page_info.has_next_page is true, more items remain to be retrieved. We can request another page of items using the page_info.end_cursor as our after query parameter.

Because this response has returned page_info.has_next_page as false, no items remain to be retrieved, and no further requests should be made. However, if page_info.has_next_page had been true, we could have again used the value of page_info.end_cursor as the value of our after query parameter to continue retrieving items.

Changelog

2024-03-29

Removes override_business_name details from the Checkout API

2022-10-17

Added aggregated_merchant_id details to the Checkout API.

2022-05-10

The Checkout API now validates decimal places following the rules described in the Amount section.

2022-03-29

Adds override_business_name details to Checkout API

2022-03-23

Initial release of Checkout and Balance APIs.