Skip to main content

Transfers (Pay-Outs)

The Transfer API (Pay-Out API) is simple to integrate, as it involves no interaction with end customers.

Making a transfer

HUB2 API reference - Create a transfer Performing a transfer only requires one call to the dedicated endpoint. Sample request:
The API responds with 201 Created and returns the transfer, including its ID: the unique identifier HUB2 creates for each transfer request.
Save the transfer ID in your database: the next steps require it.
When contacting HUB2 support about a transfer, provide the transfer ID, not your own reference.

Restrictions

For mobile money transfers to Togo (destination.country = TG), the reference field must not contain special characters. A reference with special characters is rejected with a 400 Bad Request error (code forbidden_special_chars). Allowed characters are letters, digits, hyphens, underscores, spaces and the pipe character. As a regular expression character class: [A-Za-z0-9\-_\s|].

Instant payment destination

To pay a beneficiary identified by an alias on the BCEAO instant payment system, set destination.type to instant_payment and send the beneficiary’s alias in destination.payeeAlias, with the destination.country. No provider and no msisdn are sent, and the currency must be XOF.
The account debited is your own transfer account. Instant payment is enabled per merchant by HUB2. See the instant payment guide for the alias rules, the errors and the sandbox values.

Retrying a payout safely

This section describes the behavior that applies when duplicate protection is enabled on your account. Duplicate protection is not enabled by default: HUB2 enables it for each merchant and each environment (sandbox or live). Contact HUB2 Support to enable it. Until it is enabled, HUB2 does not act on references, and sending a payout twice creates two payouts. HUB2 still records the references you send, so they already count when protection is activated: if you have reused references, switch to unique ones before asking for activation, otherwise a reused reference returns the earlier payout as a replay (200 OK, or 409 Conflict if the details differ) and creates nothing.
When a request times out or fails with a network error, you do not know whether HUB2 created the payout. With duplicate protection, you can send the same request again without paying twice. Two checks apply, both to the transfer endpoints below:
  • POST /transfers
  • POST /delegated/transfers
  • POST /delegated/transfers/irt
  • POST /irt/transfer
  • POST /irt/transfer/verified-recipient
Payouts to an instant payment destination are not covered.

Same reference

A reference identifies one payout for your account and your environment, whichever of the endpoints above created it. References are compared exactly: they are case-sensitive and spaces count, so REF and REF are two different references. Two requests carry the same payout details when they have the same destination type, destination country, beneficiary (phone number or bank account number), provider (or bank code), amount and currency. The description, the recipient name and the other fields are not compared. On a replay, the body is the original payout as it is now, with its current status. The response differs from a creation only by the 200 status and the X-Idempotent-Replay header, which is sent on a replay only.
A duplicate_reference error looks like this:
To retry a payout after a timeout, resend the exact same request with the same reference. To pay again after a payout failed, use a new reference: the same reference always returns the original payout, whatever its status.

Same details under another reference

A second check protects you from paying the same beneficiary twice under two different references. A payout is refused when a payout with the same details was created for your account and environment under another reference less than 15 minutes ago, on any of the endpoints above. The details compared are the same as above: destination type, country, beneficiary, provider (or bank code), amount and currency. A payout that failed or was canceled is ignored. This check is enabled separately from the first one, on top of it. Ask HUB2 Support to enable both. The refused payout is not created, and its reference stays free. The API responds with 409 Conflict and code possible_duplicate. This error body carries two more fields than the usual error format:
The message above is abridged.

Confirming a possible duplicate

Check the earlier payout first. If it is a mistake, do nothing. If the second payout is intended, resend it to confirm it: same endpoint, same reference, same payout details, with the rejection_id of the refusal sent in a rejectionId field of the body, before expires_at.
  • A rejectionId confirms one payout, once. If you resend the confirmed payout again, you get the original payout back as a replay (200 OK with the X-Idempotent-Replay: true header).
  • If another payout with the same details was created since the refusal, the confirmation is refused again with possible_duplicate and a new rejection_id.
  • A rejectionId that cannot confirm the payout is rejected with 422 Unprocessable Entity, code invalid_duplicate_confirmation. The same response covers every case: an unknown or expired identifier, an identifier already used, or a payout, reference or endpoint different from the refused one. Nothing is created. Resend the payout without rejectionId: it is created if it is no longer a possible duplicate, or refused with a new rejection_id.
  • A rejectionId must be ndr_ followed by 21 characters (letters, digits, _ or -). Any other value is rejected with a 400 Bad Request error.
  • Apart from its format, rejectionId is ignored unless both checks are enabled on your account.
See Error management for the error codes.

Retrieving transfer details

HUB2 API reference - Get transfer details To retrieve a transfer, call the dedicated endpoint with the transfer ID obtained when the transfer was created (see previous step). This endpoint returns the full details of a transfer. It is not meant for polling status changes. To follow the status of a transfer, poll the status endpoint (GET /transfers/{id}/status) instead. This endpoint, like every HUB2 API endpoint, is rate limited: when it is called too often, the HUB2 API responds with HTTP 429 Too Many Requests. The limits are:
  • GET /transfers/{id}: 1 request per 5 seconds per transfer
  • GET /transfers/{id}/status: 5 requests per 5 seconds per transfer
  • GET /transfers (list): 1 request per 30 seconds
Documentation about rate limits can be found here.
Sample request:
Once webhooks are configured for the transfer lifecycle, HUB2 sends a webhook for each subscribed event of the transfer. Webhooks are the recommended way to track transfer status changes. Some transfers are processed quickly, others take longer, for reasons that cannot be predicted. Fetching a transfer when its status has not changed is wasted effort.
Documentation about webhooks can be found here.

Retrieve a transfer status

This endpoint has been available since 4 February 2025.
For performance reasons, the previous endpoint is not meant for retrieving a transfer status. A dedicated endpoint serves that purpose. HUB2 API reference - Retrieve a transfer status This endpoint is also rate limited, but its limit is less restrictive than that of the previous endpoint. Sample request:
If you cannot implement webhooks, use this endpoint to fetch the status of a transfer instead.

List transfers

HUB2 API reference - List transfers To fetch a list of transfers, use the dedicated endpoint. Several query parameters (see below) filter the results. Sample request:
Query parameters used in this example:
  • from: start of the date range, here 2023-01-01T00:00:00.000.
  • to: end of the date range, here 2023-01-01T12:00:00.000.
  • page and perPage: navigation through the result pages. perPage defaults to 10 and cannot exceed 100.
By default, when no filter is set, this endpoint returns the 10 most recent transfers, ordered by creation date, descending.
When the isDelegated parameter is set, from is required and the date range must be shorter than one year, otherwise the API returns a 400 error.
For a large number of transfers, use paginated requests to retrieve all of them.

Pagination

This endpoint is paginated. The Content-Range response header gives the range of the returned results, for example 0-99/999999. The value after the slash is not an exact count of the matching transfers. To retrieve every transfer matching the filter, request the next page until a page returns fewer results than perPage. Set date filters in the initial request so the number of results stays the same while you paginate (unless to is in the future).