# Check Merchant Balance
Source: https://docs.hub2.io/api-reference/balance/check-merchant-balance
get /balance
Returns a summary of a merchant accounts balances (collection, transfer and transfer irt accounts) .
# Create a kyb
Source: https://docs.hub2.io/api-reference/compliance/create-a-kyb
post /compliance/kyb
# Create a kyb transaction
Source: https://docs.hub2.io/api-reference/compliance/create-a-kyb-transaction
post /compliance/transaction
# Delete a kyb
Source: https://docs.hub2.io/api-reference/compliance/delete-a-kyb
delete /compliance/kyb/{name}
# Delete a kyb transaction
Source: https://docs.hub2.io/api-reference/compliance/delete-a-kyb-transaction
delete /compliance/transaction/{reference}
# Fetch a kyb list
Source: https://docs.hub2.io/api-reference/compliance/fetch-a-kyb-list
get /compliance/kyb
# Fetch a kyb transactions list
Source: https://docs.hub2.io/api-reference/compliance/fetch-a-kyb-transactions-list
get /compliance/transaction
# Get kyc information
Source: https://docs.hub2.io/api-reference/compliance/get-kyc-information
get /compliance/kyc
# Retrieve a kyb by name
Source: https://docs.hub2.io/api-reference/compliance/retrieve-a-kyb-by-name
get /compliance/kyb/{name}
# Retrieve a kyb transaction by reference
Source: https://docs.hub2.io/api-reference/compliance/retrieve-a-kyb-transaction-by-reference
get /compliance/transaction/{reference}
# Update a kyb
Source: https://docs.hub2.io/api-reference/compliance/update-a-kyb
put /compliance/kyb/{name}
# Get countries
Source: https://docs.hub2.io/api-reference/data/get-countries
get /data/countries
Returns the list of countries where Hub2 is available
# Get providers
Source: https://docs.hub2.io/api-reference/data/get-providers
get /data/providers
Returns the list of providers available with Hub2
# Create IRT transfer
Source: https://docs.hub2.io/api-reference/irt/create-irt-transfer
post /irt/transfer
Make an IRT transfer
# Retrieve a IRT transfers collection
Source: https://docs.hub2.io/api-reference/irt/retrieve-a-irt-transfers-collection
get /irt/transfer
Return a list of IRT transfers, eventually filtered by criteria.
# Execute payment
Source: https://docs.hub2.io/api-reference/payment-on-terminal/execute-payment
post /terminal/payments
# Retrieve a payment
Source: https://docs.hub2.io/api-reference/payment-on-terminal/retrieve-a-payment
get /terminal/payments/{id}
# Attempt a payment on a PaymentIntent object
Source: https://docs.hub2.io/api-reference/payments/attempt-a-payment-on-a-paymentintent-object
post /payment-intents/{id}/payments
This is the endpoint to use whenever you want to attempt a payment on a payment intent.
It will create a payment object associated with the intent which will contains all the informations about the payment.
Whenever a payment fails or don't have the appropriate amount to match the intent, other payments can be created on the same intent.
# Authenticate the current payment
Source: https://docs.hub2.io/api-reference/payments/authenticate-the-current-payment
post /payment-intents/{id}/authentication
Some circuit have authentication restrictions which requires an extra step.
This endpoint is here to allow you to make that extra steps. It happens after a payment has been requested and when that payment moved to the "action_required" state.
# Create a PaymentIntent object
Source: https://docs.hub2.io/api-reference/payments/create-a-paymentintent-object
post /payment-intents
This endpoint is the first entry point of the PAY-IN circuit.
First of all, you must create a payment intent which describes your intention to collect an amount of money in a currency.
Later on, you will perform payments associated with this payment intent.
Please do save the fields returned by this endpoint, especially the field 'id' and 'token'.
That will help you identity the payment intent in our platform whenever you want to retrieve it.
# Retrieve a PaymentIntent collection
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-paymentintent-collection
get /payment-intents
This endpoints is here to provide you an efficient way of retrieving multiple payment intents at the same time.
Everything is provided in the request's headers to perform a proper pagination on the results.
# Retrieve a PaymentIntent object
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-paymentintent-object
get /payment-intents/{id}
This endpoint allows you to fetch an existing payment intent from our database.
You must either provide the "id" or the "token" for us to find that payment intent.
Retrieving using the "token" is the fastest way and uses a more permissive rate limit (10 requests per 5 seconds).
# Retrieve a PaymentIntent status
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-paymentintent-status
get /payment-intents/{id}/status
This endpoint allows you to fetch the status of an existing payment intent from our database.
You must either provide the "id" or the "token" for us to find that payment intent.
Retrieving using the "token" is the fastest way and uses a more permissive rate limit (10 requests per 5 seconds).
# Retrieve a Payments collection
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-payments-collection
get /payments
Return a list of Payments, eventually filtered by criteria.
# Retrieve configured URLs
Source: https://docs.hub2.io/api-reference/payments/retrieve-configured-urls
get /payment-intents/url/{token}
This endpoint fetch the merchant's preference and return the redirection URL saved in there.
This is mostly used for the Hub2 modal feature.
You must provide the payment intent token for that.
# Retrieve fees
Source: https://docs.hub2.io/api-reference/payments/retrieve-fees
get /payment-intents/{id}/payment-fees
This endpoint allows you to retrieve the fees associated with a payment intent.
Fees are applied per payment and this endpoints will group all the fees in an array.
# Retrieve the status payment object
Source: https://docs.hub2.io/api-reference/payments/retrieve-the-status-payment-object
get /payments/{id}/status
# Synchronously attempt a payment on a payment intent
Source: https://docs.hub2.io/api-reference/payments/synchronously-attempt-a-payment-on-a-payment-intent
post /payment-intents/{id}/payments/sync
This is the same endpoint but the payment is handled synchronously, it means it will wait for the provider response before returning the status of the payment intent. This endpoint is not the favored way to integrate our api and is not available for all merchants and for all providers.
# Create a delegated payment
Source: https://docs.hub2.io/api-reference/processing-only/make-a-delegated-payment
post /delegated/payments
This endpoint allows you to make a payment using your own provider credentials.
# Create a delegated Transfer
Source: https://docs.hub2.io/api-reference/processing-only/make-a-delegated-transfer
post /delegated/transfers
This endpoint allows you to make a transfer (PAY-OUT) using your own provider credentials.
# Create a delegated IRT transfer
Source: https://docs.hub2.io/api-reference/processing-only/make-a-delegated-transfer-irt
post /delegated/transfers/irt
Make an IRT transfer using your own provider credentials.
# Create a delegated synchronous payment
Source: https://docs.hub2.io/api-reference/processing-only/make-a-synchronous-delegated-payment
post /delegated/payments/sync
This endpoint allows you to make a synchronous payment using your own provider credentials.
# Create a Provisioning
Source: https://docs.hub2.io/api-reference/provisioning/create-a-provisioning
post /provisionings
Move funds from merchant's collection account to transfer account
# Retrieve a Provisioning from given id
Source: https://docs.hub2.io/api-reference/provisioning/retrieve-a-provisioning-from-given-id
get /provisionings/{id}
# Retrieve a Provisionings collection
Source: https://docs.hub2.io/api-reference/provisioning/retrieve-a-provisionings-collection
get /provisionings
# Retrieve a Receipt
Source: https://docs.hub2.io/api-reference/receipt/retrieve-a-receipt
get /receipts/{id}
Retrieve a sucessful or failed transfer or payment receipt
# Retrieve a Receipt as an HTML representation
Source: https://docs.hub2.io/api-reference/receipt/retrieve-a-receipt-as-an-html-representation
get /receipts/html/{id}
Retrieve a sucessful or failed transfer or payment receipt as HTML
# Create a new recipient
Source: https://docs.hub2.io/api-reference/recipients/create-recipient
post /recipient
# Delete a recipient by id
Source: https://docs.hub2.io/api-reference/recipients/delete-recipient
delete /recipient/{id}
# Get a recipient by id
Source: https://docs.hub2.io/api-reference/recipients/find-recipient
get /recipient/{id}
# Retrieve a list of recipients according to filters
Source: https://docs.hub2.io/api-reference/recipients/list-recipients
get /recipient
# Create a refund
Source: https://docs.hub2.io/api-reference/refunds/create-refund
POST https://api.hub2.io/refunds
This endpoint allows you to create a refund for a successful transfer.
Creates a refund for a previously successful transfer. The refund will be processed asynchronously and you will receive webhook notifications about status changes.
## Request
The transfer ID to refund (e.g., `tr_000000000000000000011`)
Zendesk ticket ID associated with the refund (e.g., `zd_123456`)
## Response
The unique refund ID
The current status of the refund. Possible values: `created`, `pending`, `pending_manual`, `successful`, `failed`
The original transfer ID that is being refunded
The refund amount (same as original transfer)
The currency of the refund (e.g., `XOF`, `USD`)
The reason for the refund
The payment method used for the refund. Possible values: `mobile_money`, `bank_transfer`
Whether fallback methods are allowed for this refund
Whether a fallback method was used for this refund
Additional metadata associated with the refund
The Zendesk ticket ID associated with the refund
The date and time when the refund was created
The date and time when the refund was last updated
```bash Example Request theme={null}
curl --location --request POST 'https://api.hub2.io/refunds' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function createRefund() {
const response = await fetch('https://api.hub2.io/refunds', {
method: 'POST',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
createRefund();
```
```python Python theme={null}
import json
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
}
response = requests.post(
'https://api.hub2.io/refunds',
headers=headers,
data=json.dumps(data)
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
```json Response theme={null}
{
"id": 123,
"status": "created",
"transactionId": "tr_000000000000000000011",
"amount": 2000,
"currency": "XOF",
"reason": "Customer requested refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_123456",
"createdAt": "2023-01-01T12:00:00.000Z",
"updatedAt": "2023-01-01T12:00:00.000Z"
}
```
# Retrieve a refund
Source: https://docs.hub2.io/api-reference/refunds/retrieve-a-refund
GET https://api.hub2.io/refunds/{id}
This endpoint allows you to retrieve the details of a specific refund.
Retrieves the details of a refund using its unique ID.
## Path Parameters
The unique refund ID
## Response
The unique refund ID
The current status of the refund. Possible values: `created`, `pending`, `pending_manual`, `successful`, `failed`
The original transfer ID that is being refunded
The refund amount (same as original transfer)
The currency of the refund (e.g., `XOF`, `USD`)
The reason for the refund
The payment method used for the refund. Possible values: `mobile_money`, `bank_transfer`
Whether fallback methods are allowed for this refund
Whether a fallback method was used for this refund
Additional metadata associated with the refund
The Zendesk ticket ID associated with the refund
The date and time when the refund was created
The date and time when the refund was last updated
```bash Example Request theme={null}
curl --location --request GET 'https://api.hub2.io/refunds/123' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getRefund() {
const response = await fetch('https://api.hub2.io/refunds/123', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const refund = await response.json();
console.log(refund);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getRefund();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/refunds/123',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
```json Response theme={null}
{
"id": 123,
"status": "successful",
"transactionId": "tr_000000000000000000011",
"amount": 2000,
"currency": "XOF",
"reason": "Customer requested refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_123456",
"createdAt": "2023-01-01T12:00:00.000Z",
"updatedAt": "2023-01-01T12:30:00.000Z"
}
```
# Retrieve refunds collection
Source: https://docs.hub2.io/api-reference/refunds/retrieve-refunds-collection
GET https://api.hub2.io/refunds
This endpoint allows you to retrieve a list of refunds with filtering and pagination options.
Retrieves a paginated list of refunds. You can filter the results using various query parameters.
## Query Parameters
Filter by refund ID
Filter refunds created after this date (ISO 8601 format, e.g., `2023-01-01T00:00:00.000Z`)
Filter refunds created before this date (ISO 8601 format, e.g., `2023-01-01T12:00:00.000Z`)
Filter by refund status. Possible values: `created`, `pending`, `pending_manual`, `successful`, `failed`
Filter by transfer reference (original transfer reference)
Page number for pagination
Number of results per page (max 100)
## Response
Array of refund objects
The unique refund ID
The current status of the refund
The original transfer ID that is being refunded
The refund amount
The currency of the refund
The reason for the refund
The payment method used for the refund
Whether fallback methods are allowed
Whether a fallback method was used
Additional metadata associated with the refund
The Zendesk ticket ID associated with the refund
The date and time when the refund was created
The date and time when the refund was last updated
Pagination information
Current page number
Number of results per page
Total number of refunds
Total number of pages
```bash Example Request theme={null}
curl --location --request GET 'https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getRefunds() {
const response = await fetch('https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const refunds = await response.json();
console.log(refunds);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getRefunds();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
```json Response theme={null}
{
"data": [
{
"id": 123,
"status": "successful",
"transactionId": "tr_000000000000000000011",
"amount": 2000,
"currency": "XOF",
"reason": "Customer requested refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_123456",
"createdAt": "2023-01-01T12:00:00.000Z",
"updatedAt": "2023-01-01T12:30:00.000Z"
},
{
"id": 124,
"status": "pending",
"transactionId": "tr_000000000000000000012",
"amount": 1500,
"currency": "XOF",
"reason": "Merchant initiated refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_789012",
"createdAt": "2023-01-01T11:30:00.000Z",
"updatedAt": "2023-01-01T11:30:00.000Z"
}
],
"pagination": {
"page": 1,
"perPage": 100,
"total": 2,
"totalPages": 1
}
}
```
# Get a list of sms sent
Source: https://docs.hub2.io/api-reference/sms/get-a-list-of-sms-sent
get /sms/list
Retrieve a list of sms sent between two date.
# Retrieve number of sms sent
Source: https://docs.hub2.io/api-reference/sms/retrieve-number-of-sms-sent
get /sms/count
Retrieve number of sms sent between two date
# Send Sms
Source: https://docs.hub2.io/api-reference/sms/send-sms
post /sms
Send Sms to different destinations.
# Create Transfer
Source: https://docs.hub2.io/api-reference/transfers/create-transfer
post /transfers
This endpoint allows you to make a transfer (PAY-OUT)
# Retrieve a Transfer
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfer
get /transfers/{id}
This endpoint allows you to fetch an existing transfer from our database
by providing its 'id'
# Retrieve a Transfer collection
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfer-collection
get /transfers
Return a list of Transfers, eventually filtered by criteria.
# Retrieve a transfer's balance
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfers-balance
get /transfers/{id}/balance
Fetch balance before and after transfer
# Retrieve a transfer's status
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfers-status
get /transfers/{id}/status
Retrieve the status for a transfer
# Create a webhook
Source: https://docs.hub2.io/api-reference/webhooks/create-a-webhook
post /webhooks
# Get a webhook
Source: https://docs.hub2.io/api-reference/webhooks/get-a-webhook
get /webhooks/{id}
# Get all webhooks
Source: https://docs.hub2.io/api-reference/webhooks/get-all-webhooks
get /webhooks
# Remove a webhook
Source: https://docs.hub2.io/api-reference/webhooks/remove-a-webhook
delete /webhooks/{id}
# Update a webhook
Source: https://docs.hub2.io/api-reference/webhooks/update-a-webhook
patch /webhooks/{id}
# Error management
Source: https://docs.hub2.io/documentation/en/error_management
## Error format
The error format in HUB2 API is being standardized.
Below are the fields in the standardized response along with an example:
| Field name | Description |
| :----------- | :--------------------------------------------------------------- |
| `status` | The HTTP status |
| `code` | A business error code internal to HUB2 API (see full list below) |
| `message` | A readable description of the error code |
| `request_id` | Unique request identifier |
Currently, it is necessary to set the header 'x-use-standardized-error' === true to obtain this standardized format.
Starting from October the 1st of 2024, this error format will be the only one returned by the HUB2 API.
Therefore, merchants needs to update their integration before this deadline.
## Sample : Error 404
```json theme={null}
{
"status": 404,
"code": "Not Found",
"message": "Country not found",
"request_id": "req_RRg_xtA55q32PLIWYVLFL"
}
```
**Note**: the field `request_id` is a unique request identifier that will allow HUB2 Support team to respond more quickly in case of help requests.
## Error codes
### HTTP status codes
HTTP status codes do not indicate application errors.
Standard HTTP status codes that can be returned by the HUB2 API :
| Status Code | Status Text | Description |
| :------------------------- | :-------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | `OK` | The HTTP request was completed as expected. |
| `400` | `Bad Request` | The request was unacceptable, often due to a missing required parameter. |
| `401` | `Unauthorized` | No valid API key provided. |
| `402` | `Request Failed` | The parameters were valid but the request failed. |
| `403` | `Forbidden` | The API key doesn’t have permissions to perform the request. |
| `404` | `Not Found` | The requested resource doesn’t exist. |
| `409` | `Conflict` | The client's request is in direct conflict with another action that is already in progress or has been completed |
| `429` | `Too Many Requests` | Excessive requests have been made to the API in a short amount of time. The request wasn't accepted to preserve overall API performance |
| `500`, `502`, `503`, `504` | Server-related errors | Something went wrong on HUB2’s end. |
### Application error codes
Application error codes that may be returned by the HUB2 API :
| Code | Reason |
| :-------------------------- | :------------------------------------------------------------------------------------------------------------------ |
| `account-already-exists` | An account 'accountType' in mode (live or sandbox) already exists for the merchant merchantId |
| `cancellation-already-done` | The transaction id has already been canceled. |
| `insufficient-funds` | The account does not have enough funds to transfer amount currency. |
| `invalid-currency` | Invalid currency (currency). |
| `locked-account` | The account is locked. |
| `transaction-not-found` | The transaction transactionId for the account accountId is not found. |
| `currency-not-found` | Currency not found (currency). |
| `withdrawal-unavailable` | Withdrawals are unavailable on the account accountId. The account may be blocked or does not have sufficient funds. |
| `missing-funds-reservation` | Insufficient reserved funds. |
| `merchant-not-found` | The merchant is not found. |
| `unsupported-command` | Unsupported command. |
| `no_auth_required` | The transaction id does not require authentication. |
| `transaction_canceled` | The transaction (payment) has been canceled. No payment can be processed. |
| `transaction_missing` | The transaction (transfer or payment) is not found. |
| `transaction_in_progress` | The transaction (transfer or payment) is already in progress. |
| `transaction_done` | The transaction (transfer or payment) has already been completed. |
# Glossary
Source: https://docs.hub2.io/documentation/en/glossary
Definition of useful terms used in HUB2's payment platform
## Collection Account
Account used for payments.
## Transfer Account
Account used for transfers.
## IRT Transfer Account
Account used for IRT.
## IRT (International Remittance Transfers)
The cross-border transfer of funds from one person to another.
## Payment
A payment (or PAY-IN) is a financial transaction in which money is moved from the user's account to the merchant's collection account.
## Transaction polling
A method of periodically retrieving the status of a transaction by regularly querying an API for updates. [Polling vs. Webhooks](https://blog.bytebytego.com/i/141963246/polling-vs-webhooks)
## Balance
Difference between total debit transactions (transfers/pay-outs, withdrawals, fees) and total credit transactions (payments collected/pay-ins, deposits). The difference cannot be negative. Consequently, the balance is a credit balance.
## Transfer
A transfer (or PAY-OUT) is a financial transaction in which money is moved from the merchant's transfer account to the user's account.
## Webhook
Sending notifications to the merchant when specific events occur. [Polling vs. Webhooks](https://blog.bytebytego.com/i/141963246/polling-vs-webhooks)
## IPN
Instant Payment Notification. Webhooks sent by providers to HUB2.
## KYB
Know Your Business - A declaration in which an aggregator merchant provides informations about a merchant he aggregates. This prior declaration is necessary to use the [bank collection feature](/integration/en/other_products/bank_collection)
## KYB transaction
Declaration made by a merchant-aggregator when making a bank transfer to HUB2 to refill its account.
This information enables HUB2's teams to check the conformity of the transaction and validate it.
## KYC
Know Your Customer - A process for verifying the identity of a company's customers.
## Submerchant
An account linked to a PSP or Fintech Merchant account that is granted a unique ID to process transactions. This structure ensures compliance with regulatory requirements.
# Rate limits
Source: https://docs.hub2.io/documentation/en/limits
Those are the limits applied on our API.
This documentation was last updated on **23-11-21**.
## Introduction
As the HUB2 API is their main project, a solid quality of service is required.
In order to keep an appropriate latency and a reliable service, rate limits must be enforced on the most cost-heavy endpoints.
This limit is applied per IP address, so it is relative to each of the API users and the usage of one won't impact the usage of the other.
That is why we enforce those limits.
## API limits
Here is a table of rate limits. The global limit is for all other endpoints.
| `GET` | `/transfers` | 1 req / 30 sec | 2 req / min | |
| :----- | :------------------------------------- | :-------------- | :------------ | :----------- |
| `POST` | `/transfers` | 75 req / 10 sec | 400 req / min | |
| `GET` | `/transfers/:id` | 1 req / 5 sec | 12 req / min | *\*per :id* |
| `GET` | `/transfers/:id/balance` | 1 req / 10 sec | 6 req / min | |
| `GET` | `/transfers/:id/status` | 5 req / 5 sec | 60 req / min | *\*per :id* |
| `GET` | `/payments` | 1 req / 30 sec | 2 req / min | |
| `GET` | `/payments_intents` | 1 req / 30 sec | 2 req / min | |
| `POST` | `/payments_intents` | 75 req / 10 sec | 400 req / min | |
| `GET` | `/payments_intents/:id` | 1 req / 5 sec | 12 req / min | *\*per :id* |
| `POST` | `/payments_intents/:id/authentication` | 30 req / 5 sec | 360 req / min | |
| `POST` | `/payments_intents/:id/payments` | 75 req / 10 sec | 400 req / min | |
| `POST` | `/payments_intents/:id/payments/sync` | 75 req / 10 sec | 400 req / min | |
| `GET` | `/payments_intents/:id/payment-fees` | 75 req / 10 sec | 450 req / min | |
| `GET` | `/payments/:id/status` | 5 req / 5 sec | 60 req / min | *\*per :id* |
| `GET` | `/payments_intents/:id/status` | 5 req / 5 sec | 60 req / min | *\*per :id* |
| `POST` | `/terminal/payments` | 5 req / 10 sec | 30 req / min | |
| `GET` | `/terminal/payments/:id` | 5 req / 10 sec | 30 req / min | |
| `GET` | `/balance` | 75 req / 10 sec | 450 req / min | |
| `*` | `*` | 50 req / 5 sec | 600 req / min | Global limit |
**Actual limit** is how the code handles it in termes of requests / seconds.
**Rationalized limit** ease understanding and helps compare the different values on a same scale.
**This limit applies both to mode `sandbox` and mode `live`.
Remember to stop sandbox traffic if the transaction stream is getting heavy.**
## How to handle limits
Whenever a request is received by the API beyond the limit of the endpoint, an error `Too Many Requests` with HTTP status `429` will be returned.
Please checkout [MDN documentation](https://developer.mozilla.org/en/docs/Web/HTTP/Status/429) about this.
The request failed and was not handled by the API because of the rate limit.
Headers are provided in the HTTP response for proper handling :
| Header name | Description |
| :---------------------- | :--------------------------------------------------------------------------------------------------------- |
| `Retry-After` | In case the limit is reached,
this header tells how long to wait before a new request will be allowed |
| `X-RateLimit-Limit` | The current limit on the endpoint |
| `X-RateLimit-Remaining` | Number of remaining requests before reaching the limit |
| `X-RateLimit-Reset` | Time before a spot is free in the queue for a new request |
### Reactive solution (easier)
One way to handle rate limits from a client-side perspective is to retry requests if they fail for a 429 reason:
```javascript hub2-api.js theme={null}
const MAXIMUM_TRY_COUNT = 3;
const URL = 'https://api.hub2.io/balance;
const CONFIGURATION = {
method: "GET",
headers: {
ApiKey: MY_API_KEY,
MerchantId: MY_MERCHANT_ID,
},
};
(function requestRetry(numberOfTries) {
fetch(URL, CONFIGURATION)
.then(() => {
// code here
})
.catch((error) => {
if (error?.response?.status === 429) {
if (numberOfTries > MAXIMUM_TRY_COUNT) {
throw error;
}
// Adding 1 second of grace delay
const retryAfter: number = parseInt(response.headers['retry-after'], 10) + 1;
await new Promise((resolve) => setTimeout(resolve, retryAfter));
return requestRetry(numberOfTries + 1);
}
})
})(0);
```
```typescript hub2.service.ts theme={null}
// Using framework NestJS
import { HttpService } from "@nestjs/axios";
import { Injectable } from "@nestjs/common";
import { lastValueFrom } from "rxjs";
@Injectable()
export class HUB2Service {
public static MAXIMUM_TRY_COUNT: number = 3;
constructor(
private readonly http: HttpService,
) {}
async requestRetry(numberOfTries: number = 0): Promise {
const url = 'https://api.hub2.io/balance;
const config = {
headers: {
ApiKey: MY_API_KEY,
MerchantId: MY_MERCHANT_ID,
},
};
try {
const response = await lastValueFrom(this.http.post(url, {}, config));
} catch (error) {
if (error?.response?.status === 429) {
if (numberOfTries > HUB2Service.MAXIMUM_TRY_COUNT) {
throw error;
}
// Adding 1 second of grace delay
const retryAfter: number = parseInt(response.headers['retry-after'], 10) + 1;
await new Promise((resolve) => setTimeout(resolve, retryAfter));
return this.requestRetry(numberOfTries + 1);
}
}
}
}
```
### Proactive solution (harder)
The proactive solution is a bit trickier, it consist of keeping a pool of requests ready to be started with the exact size of the destination endpoint rate limit. Whenever the pool is empty, the next request waits in line for a token to free up.
Check out [that interesting article](https://www.useanvil.com/blog/engineering/throttling-and-consuming-apis-with-429-rate-limits/) on how to implement rate limit from the client-side perspective, especially approaches 4 and 4.1.
## Conclusion
In a perfect world, no limit would be set on the API endpoints. However in the real world, it helps preventing abuse and keep a reliable service for everyone.
The team work on a daily basis to improve the stability and the performance of the API and this page will be updated as soon as upgrades allow to loosen the limits.
# Policies
Source: https://docs.hub2.io/documentation/en/policies
There are constraints to comply when making a transaction, depending on the country and the provider :
* `amount` must be set between the minimum and maximum amount values (inclusively) listed in the below table (MIN & MAX columns)
* `amount` must be a multiple of the value listed in the below table (MULTIPLE column)
## PAY-IN
### Mobile Money
| GATEWAY | COUNTRY | PROVIDER | CURRENCY | MIN | MAX |
| :------ | :------ | :------- | :------- | :-- | :-------- |
| Fedapay | BJ | Moov | XOF | 100 | 3 000 000 |
| Fedapay | BJ | MTN | XOF | 100 | 3 000 000 |
| Fedapay | TG | Moov | XOF | - | - |
| Fedapay | TG | Togocell | XOF | - | - |
| Intouch | BF | Mobicash | XOF | - | - |
| Intouch | BF | Orange | XOF | - | - |
| Intouch | CI | Moov | XOF | - | - |
| Intouch | CI | MTN | XOF | - | - |
| Intouch | CI | Orange | XOF | - | - |
| Intouch | CM | MTN | XAF | 500 | 500 000 |
| Intouch | CM | Orange | XAF | 500 | 500 000 |
| Intouch | GA | Airtel | XAF | - | - |
| Intouch | GA | Mobicash | XAF | - | - |
| Intouch | GN | MTN | GNF | - | - |
| Intouch | GN | Orange | GNF | - | - |
| Intouch | ML | Mobicash | XOF | - | - |
| Intouch | ML | Orange | XOF | - | - |
| Intouch | SN | Emoney | XOF | - | - |
| Intouch | SN | Free | XOF | - | - |
| Intouch | SN | Orange | XOF | - | - |
* For BF, CM, SN and ML, a rounding fee (paid by customer) is applied to reach a modulo 5 amount
* For GN, a rounding fee (paid by customer) is applied to reach a modulo 1000 amount
## PAY-OUT
| PROVIDER | COUNTRY | MIN | MAX | MULTIPLE |
| :------- | :------ | :----- | :-------- | :------- |
| Orange | BF | 100 | 1 000 000 | 5 |
| Mobicash | BF | 500 | 1 000 000 | 5 |
| Orange | CI | 200 | 1 500 000 | 5 |
| MTN | CI | 100 | 2 000 000 | 5 |
| Moov | CI | 500 | 2 000 000 | 5 |
| Orange | CM | 500 | 500 000 | 5 |
| MTN | CM | 500 | 500 000 | 5 |
| Airtel | GA | 100 | 1 000 000 | 5 |
| Mobicash | GA | 100 | 1 000 000 | 5 |
| Orange | GN | 10 000 | 1 500 000 | 1 000 |
| MTN | GN | 10 000 | 1 500 000 | 1 000 |
| Orange | ML | 500 | 1 500 000 | 5 |
| Mobicash | ML | 500 | 1 500 000 | 5 |
| Orange | SN | 100 | 1 500 000 | 5 |
| Free | SN | 100 | 1 500 000 | 5 |
| EMoney | SN | 100 | 1 500 000 | 5 |
# API keys
Source: https://docs.hub2.io/integration/en/getting_started/api_keys
## Introduction
API keys are used as the main authentication mechanism for the HUB2 API, therefore it's really important to understand what they are and how to use them.
API Key management is accessible on the [HUB2 Dashboard](https://dashboard.hub2.io), in the "Developer" section, as illustrated below:
The following screenshot shows the list of existing keys:
## Keys management
### Creating a key
The API key is only visible when it is created and **can no longer be consulted after this stage**, for security reasons.
Click on the "**Create a key**" button to access the following window:
* (Optional) Enter a **name** and a **description** for the new key.
* The parameter **`environment`** (*sandbox* or *live*) is required. The new key will be restricted to this environment. [Read more about environments.](#environment)
In this section, it's possible to restrict which originating IP addresses can use the newly created API key.
Permissions of the new API key can be set here.
Permissions set on an API key allows the segregation of responsibilities and advanced configuration. This section allows a merchant to use different API keys, whether its software has several components, each with a different role, or not.
On the summary page, review the settings for the new API key, and then, click *Validate*.
The API key is only displayed once. Save it now in a secure vault. It will not be accessible later.
### Editing a key
On each API key row, in the "Actions" column, an *Edit* button is available to change the settings of a key. This edition process is the same as the creation process, except that you will not be able to view the key. **Every setting can be edited**.
Changes to permissions and IP address restrictions are taken into account as soon as they are modified. Special attention is required before committing changes.
### Deleting a key
In the "Actions" column, a *Delete* button is available to delete a key.
Deleting a key is irreversible. HTTP traffic using the deleted key will be stopped immediately after deletion.
## Using the keys
In the [API reference](/api-reference), the endpoints requiring authentication by merchant ID and API key are listed. For these endpoints, HTTP headers must be configured in the HTTP request to identify and authenticate the emitter.
## API key configuration
### Environment
* `sandbox`: This is a closed environment for integration testing. No traffic or real world transactions will be created if the API key used is set to the `sandbox` environment. Provider behaviour is simulated by HUB2. Also, the transfer and collection accounts used for `sandbox` transactions will be the `sandbox` accounts.
* `live` : *NB: Requires GO LIVE and an integration review by HUB2 before traffic can be sent in a `live` (real world) environment*. This is the real world environment, a key in this environment allows real traffic to be processed and providers will be contacted if the transaction endpoints are called.
[More on transfer and collection accounts.](/integration/en/getting_started/api_operation#accounts)
### IP address restriction
IP address restriction is an optional feature available to merchants to further secure exchanges between the merchant platform and HUB2.
When an IP address restriction has been set on an API key, HUB2 checks that the IP address originating the HTTP request is authorized to use the API key it contains.
### Permissions
Configurable permissions per API key allows merchants to create multiple keys with different permissions, so that each key has a different responsibility.
The full list of permissions and their description is as follows:
| Permission | Description | |
| :------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | - |
| `Api.transfer_create` | Allows the creation of transfers | |
| `Api.transfer_read` | Allows the reading of transfers | |
| `Api.payment_intent_create` | Allows the creation of payment intents | |
| `Api.payment_intent_read` | Allows the reading of payment intents | |
| `Api.payment_intent_auth_create` | Allows the authentication of a payment | |
| `Api.payment_fees_read` | Allows the reading of payment fees | |
| `Api.payment_create` | Allows the creation of a payment in a payment intent | |
| `Api.provisioning_read` | Allows the reading of provisioning requests | |
| `Api.provisioning_create` | Allows the creation of a provisioning request | |
| `Api.merchant_balance_read` | Allows the reading of transfer and collection accounts balance | |
| `Api.terminal_payment_create` | Allows the creation of payments to be made via payment terminal | |
| `Api.terminal_payment_read` | Allows the reading of payments made via payment terminal | |
| `Api.kyb_read` | Allows the reading of [kyb](/documentation/en/glossary#kyb) / [kyb-transaction](/documentation/en/glossary#kyb-transaction) | |
| `Api.kyb_update` | Allows the update of kyb | |
| `Api.kyb_delete` | Allows the deletion of kyb / kyb-transaction | |
| `Api.kyb_create` | Allows the creation of kyb / kyb-transaction | |
## Good practices
### Security
A good security practice is to **renew API keys on a regular basis**. If a key has been inadvertently disclosed to a third party, or stolen by a malicious third party, deleting the keys concerned and recreating new ones can limit the impact on a merchant's business.
**Disclaimer**: The merchant is responsible for the manner in which the API Key has been securely stored on its platform. HUB2 cannot be held responsible for the processing of illegitimate traffic associated with a Merchant API Key that has been stolen or disclosed.
# API Operation
Source: https://docs.hub2.io/integration/en/getting_started/api_operation
## HUB2 APIs
The **Pay-in** & **Pay-out** APIs developed by HUB2 are different in the way they operate.
### Pay-out API
The [Pay-out API](/integration/en/transfers) is used to transfer funds to a third party: the transaction is a debit.
The funds are automatically sent to the recipient without any interaction on their part.
The [transfer account](/documentation/en/glossary#transfer-account) must be funded before transfers can be made using the HUB2 API. Before proceeding with the transfer, HUB2 checks whether the merchant has the necessary funds and reserves them until the provider confirms the actual transfer of the funds to the recipient.
### Pay-in API
The [Pay-in API](/integration/en/payments) is used to collect funds from a third party (end customer): the transaction is of the credit type.
Customer interaction is required here, since the customer must, depending on the case :
* confirm the transaction with their provider when they receive a PUSH message or execute a USSD code
* first generate an authentication code with the provider, to be provided at the time of payment.
Once the transaction has been authenticated, the amount of the transaction is credited to the merchant's [collection account](/documentation/en/glossary#collection-account).
## Accounts
As mentioned above, pay-ins (payments) and pay-outs (transfers) use distinct accounts to operate. All successful pay-ins are credited to the collection account and all successful pay-outs are deducted from the transfer account.
**Important :** Since transactions may exist in a `live` or `sandbox` environment, collection and transfer accounts also exist in both environments. As a consequence, a transaction that is created in a `sandbox` environment will only affect the `sandbox` account and thus, the `sandbox` balance. Same for the `live` environment.
When a new merchant is created on the HUB2 platform, and since HUB2 simulates the provider's behavior in the `sandbox` environment, the `sandbox` transfer account is **credited with 100,000 XOF** to help the integration process.
## Asynchronicity
All transactions created on the HUB2 API, with the exception of transactions created on the [synchronous circuit](#synchronous-circuit), are processed asynchronously.
Once accepted, the parameters of a transaction request are recorded in a stack of transactions to be processed and are then handled by the other components of the HUB2 infrastructure.
This mode of operation has a number of advantages, not least of which is the ability to respond very quickly to the sender of the HTTP request, thereby reducing the time spent waiting on the merchant platform side, and thus reducing its load.
## Circuits
### Asynchronous circuit
All transactions initiated in this circuit are processed asynchronously by the HUB2 API. This is the default mode of operation for providers, who do not respond directly to transaction requests but place them in a queue for processing. Transactions are then updated once they have been processed.
The time taken for a provider to process a transaction may vary from one provider to another.
Updating the status of a transaction to a final state at HUB2 therefore depends on the transaction status update at the provider.
Once a transaction has been assigned a final status, it can be updated at HUB2 in one of two ways:
* either by an [Instant Payment Notification](/documentation/en/glossary#ipn) sent by the provider
* or by [regularly querying the status of the transaction](/documentation/en/glossary#transaction-polling) by HUB2 at the provider
### Synchronous circuit
The [asynchronous](#asynchronicity) operating mode of HUB2 is not available with the synchronous circuit. The sender of the HTTP request (the merchant) must wait for a response from HUB2, which will depend on both the provider contacted and the responsiveness of the end customer.
This circuit is only available for pay-ins (payments).
Some providers offer the option, either by default or as an additional circuit, of operating synchronously. In this mode of operation, the transaction request is processed immediately when it is called, without being placed in a stack of transactions to be processed later.
The final status of a transaction is obtained directly when the provider responds, although there may be a non-negligible delay between HUB2 sending the request and the provider responding.
The providers for which the synchronous circuit is available are :
* Wave (all countries)
This circuit can only be activated on request to HUB2 Support. Please contact HUB2 Support for more information.
# Introduction
Source: https://docs.hub2.io/integration/en/getting_started/introduction
A guide to help getting started with HUB2 API integration
*Go to the full documentation of REST API endpoints, parameters and responses returned - **[API Reference](/api-reference)***
HUB2 [Transfer (Pay-out) API](/integration/en/transfers) & [Payment (Pay-in) API](/integration/en/payments) solutions offer a standardized and secure integration for online financial transactions. These solutions support various payment methods, such as :
* Mobile money
* Bank transfers
* Debit or credit cards
To begin integrating the HUB2 API, it is crucial to follow the steps and concepts detailed in this Getting Started Guide, which lay the foundation for the rest of the integration process.
[A glossary](/documentation/en/glossary) is also available to help understand the terms used in this documentation.
# Security
Source: https://docs.hub2.io/integration/en/getting_started/security_considerations
# Security considerations
Security remains the top priority when it comes to online financial transactions.
## Integration
To ensure that exchanges between the merchant and HUB2 remain confidential, and that only the merchant is at the origin of the transactions (and therefore of the amount and other properties of these transactions), **the integration of the API must only take place on the server side**.
Sensitive data, such as API keys, **must never be disclosed**, whether on a website or in a mobile application.
This approach strengthens security by avoiding direct exposure of API keys and sensitive information on the client side. By performing transactions on the server side, the risk of malicious attacks, such as client-side data manipulation, is considerably reduced.
## CORS
To protect against potential client-side integrations, the HUB2 API is set up so that it can never be loaded from the end-client's browser.
In other words, the HTTP headers for *cross origin resource sharing* ([CORS](https://developer.mozilla.org/fr/docs/Web/HTTP/CORS)) have been correctly set.
# Signature
To enhance the security and integrity of API interactions, an optional payload signature mechanism is available. This feature enables merchants to cryptographically sign the JSON request body sent to the API. By verifying this signature, it can be ensured that the data has not been tampered with in transit and originates from a trusted source. This added layer of security is particularly valuable for sensitive financial transactions.
Check out the [integration page](/integration/en/other_products/payload_signature) to understand how to use out.
# Platform security mechanisms
To protect the HUB2 platform, several security components are implemented at different levels of the infrastructure.
At the perimeter, a *Web Application Firewall* (WAF) analyzes every single HTTP request before forwarding it to the backend. Its primary role is to detect and block malicious HTTP requests before they can reach the application layer.
For **rejected requests**, clients will face the following behavior:
Here, an SQL injection (SQLi attack) is simulated on the HUB2 API.
The web application firewall immediately rejects the request and returns a response with the status `HTTP 403 Forbidden`.
**Note:** The WAF will *always* return a `text/html` response Body.
**Please keep this in mind when integrating the HUB2 API.**
### Troubleshooting `HTTP 403 Forbidden` common errors
If a legitimate request faces an `HTTP 403 Forbidden`, please first follow the below guidelines to ensure your integration complies with our security standards.
If your request is still rejected after applying the below guidelines, feel free to file a ticket with the HUB2 Support team.
#### 1. `GET` HTTP request with Body content
* `GET` requests must **NOT** include any Body content. All non-compliant requests will be rejected by the HUB2 security gateway.
* The `Content-Length` header is not mandatory for a `GET` request. However, if included, it must be set to `0` and the body must remain completely empty.
For example:
Notice the `Content-Type` & `Content-Length` headers sent, along with the **Body content** for a `GET` request. Such requests will be rejected.
```http theme={null}
GET /webhooks/ HTTP/1.1
Host: api.hub2.io
Accept: application/json
User-Agent: Example Merchant backend/1.0
Content-Type: application/json <------------ Here
Content-Length: 184 <----------------------- Here
{"url":"https://my.webhook.target","events":["payment.created","payment_intent.created"],"description":"This is a webhook trigger upon payment & payment_intent creation","metadata":{}}
^----------- Here
```
See the ***Good practice*** below to fix this.
Apply the following recommendations to avoid triggering the security gateway:
1. Remove the Body content entirely.
2. Remove the `Content-Type` & `Content-Length` headers (`Content-Length` is generally computed automatically by your HTTP client library, so there is usually nothing extra to configure).
```http theme={null}
GET /webhooks/ HTTP/1.1
Host: api.hub2.io
Accept: application/json
User-Agent: Example Merchant backend/1.0
```
#### 2. Non-stringified JSON body
All HTTP `POST` requests using `application/json` must be **stringified** as a **single line of text**. Multi-line JSON payloads will be rejected by the HUB2 security gateway. Formatting the JSON string with carriage returns (CR/LF) or spaces (except inside the values themselves) can trigger filtering rules.
For example:
Notice the **multi-line JSON object** in the Body content below:
```http theme={null}
POST /webhooks/ HTTP/1.1
Host: api.hub2.io
Accept: application/json
User-Agent: Example Merchant backend/1.0
Content-Type: application/json
Content-Length: 214
v----------- Here
{
"url": "https://my.webhook.target",
"events": [
"payment.created",
"payment_intent.created"
],
"description": "This is a webhook trigger upon payment & payment_intent creation",
"metadata": {}
}
```
See the ***Good practice*** below to fix this.
To avoid triggering the security gateway, transform the multi-line Body content into a **single line of text**, as follows:
```http theme={null}
POST /webhooks/ HTTP/1.1
Host: api.hub2.io
Accept: application/json
User-Agent: Example Merchant backend/1.0
Content-Type: application/json
Content-Length: 184
{"url":"https://my.webhook.target","events":["payment.created","payment_intent.created"],"description":"This is a webhook trigger upon payment & payment_intent creation","metadata":{}}
```
#### 3. Body content differs from the `Content-Type` header
The HUB2 API currently only accepts JSON payloads. If an HTTP request is sent with valid JSON content, but the `Content-Type` header is set to a different value (e.g., `text/plain`, `text/xml`, or `application/x-www-form-urlencoded`), the security gateway will parse it incorrectly and block the request.
For example:
Notice the incorrect `Content-Type` header given the JSON payload:
```http theme={null}
POST /webhooks/ HTTP/1.1
Host: api.hub2.io
Accept: application/json
User-Agent: Example Merchant backend/1.0
Content-Type: application/x-www-form-urlencoded <------------ Here
Content-Length: 184
{"url":"https://my.webhook.target","events":["payment.created","payment_intent.created"],"description":"This is a webhook trigger upon payment & payment_intent creation","metadata":{}}
```
See the ***Good practice*** below to fix this.
Always set the `Content-Type` header **to match the Body content** format:
```http theme={null}
POST /webhooks/ HTTP/1.1
Host: api.hub2.io
Accept: application/json
User-Agent: Example Merchant backend/1.0
Content-Type: application/json <------------ Here
Content-Length: 184
{"url":"https://my.webhook.target","events":["payment.created","payment_intent.created"],"description":"This is a webhook trigger upon payment & payment_intent creation","metadata":{}}
```
# Understand errors
Source: https://docs.hub2.io/integration/en/getting_started/understand_errors
To better understand what could cause a failure, it is necessary to distinguish between errors reported at HTTP protocol level via HTTP status codes and the ones reported at application level (HUB2 API).
## HTTP error status codes
A HTTP status code in "error" (`HTTP 4XX` or `HTTP 5XX`) indicates a communication issue or request processing issue between an HTTP client and an HTTP server. These codes, included in the server response, indicate the status of the HTTP request. These codes are standardised and must be followed by the server-side application in order to be correctly interpreted by the HTTP client.
## Application errors
On the other hand, there are "application errors" which occur once the HTTP request has been accepted for asynchronous processing (after it has returned an `HTTP 2XX` status code). These errors are specific to the logic of the HUB2 platform and are communicated via specific error codes defined within the HUB2 application.
See also :
* [Failure causes on Transfers](/integration/en/transfers/transfers_failure_causes) (Pay-outs)
* [Failure causes on Payments](/integration/en/payments/payments_failure_causes) (Pay-ins)
## Fix an error
It is important to distinguish between the two types of errors.
Understand that HTTP status codes in error indicates an issue *during communication (or with the request) between the client and the server*, whereas application errors reflect issues encountered during the asynchronous processing of a transaction, *outside the context of an HTTP request*, and only at HUB2 level.
The reasons for failure listed on the [Application errors pages](#application-errors) are only for errors encountered during asynchronous processing of any HTTP request. For more details about which HTTP status code can be returned by the HUB2 API for each endpoint, please refer to the [HUB2 API Reference Documentation](/api-reference).
# Balances
Source: https://docs.hub2.io/integration/en/other_products/balances
The accounts balances are available at any time using the HUB2 API.
Please check the endpoint in the [API reference](/api-reference/balance/check-merchant-balance).
# Request
Here is a sample of a request to that endpoint :
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/balance?currency=XOF&date=2023-10-01T00:00:00.000Z' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getBalance() {
const response = await fetch('https://api.hub2.io/balance?currency=XOF&date=2023-10-01T00:00:00.000Z', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox'
}
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getBalance();
```
```python Python theme={null}
import requests
url = 'https://api.hub2.io/balance?currency=XOF&date=2023-10-01T00:00:00.000Z'
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
Please note the following parameters :
* Parameter `currency` : identify the currency to fetch, here the value is `XOF`
* Parameter `date` : identify the date for which the balance will be fetched, here the value is `2023-10-01T00:00:00.000Z`, default value is now if the field is missing
# Response
Here is a sample of the response in JSON format :
```json theme={null}
{
"collectionAccount": [
{
"amount": 100,
"availableBalance": 100,
"currency": "XOF",
"date": "2023-10-01T00:00:00.000Z"
}
],
"transferAccount": [
{
"availableBalance": 100,
"reservedBalance": 100,
"currency": "XOF",
"date": "2023-10-01T00:00:00.000Z",
"amount": 100
}
]
}
```
# Bank collection
Source: https://docs.hub2.io/integration/en/other_products/bank_collection
## Introduction
This feature is dedicated to merchant aggregators and allows collection via bank transfers.
## Life cycle
Here's a schema of the process.
1. Merchant must declare the business of the sub-merchant
2. Then they must declare each transaction using their reference
3. The bank transfer must occur with the same reference
4. An HUB2 agent will check the data upon reception of the transfer and validate or reject the transaction
5. Once validated, merchant will be notified with a webhook on event `transaction.deposit`
## Compliance
### Declare merchants business
First of all, before anything else, the aggregator must declare information about their merchants.
Multiple endpoints are available to manage the aggregated merchants.
Please check these endpoints in the [API reference](/api-reference/compliance).
In order to keep track of a full history, please keep in mind that deleted data are kept in HUB2 database for compliance reasons. That's also the case when editing a merchant information, previous data are kept.
### Declare transaction
Whenever a merchant will send a bank transfer to HUB2 with the reference, the aggregator must also call HUB2 API to declare information about this transaction.
Please check these endpoints in the [API reference](/api-reference/compliance/create-a-kyb-transaction).
* The transaction field `merchantName` is here to know for which merchant this transaction is accountable
* The field `reference` is the one typed in the bank transfer
* The `IBAN` of the destination account must be provided here
This is not a synchronous process, the aggregator can call the HUB2 API before of after the bank transfer occurs, it does not matter here.
## Internal process
Whenever a bank transfer gets on the HUB2 accounts, cash management and control teams will perform checks:
* Using the reference in the bank transfer, they will seek the declared transaction associated with this reference
* Once found, they will perform checks on the information such as amount and currency
* Once validated, they will perform a deposit on the aggregator's account on HUB2 platform
From there, the aggregator can be notified using the webhook event `transaction.deposit` which will contains the reference.
# Payload signature
Source: https://docs.hub2.io/integration/en/other_products/payload_signature
# Introduction
This section details how to integrate the optional payload signature mechanism to ensure the integrity and authenticity of API requests.
## Prerequisites
Before utilizing the payload signature feature, the following steps must be completed:
1. Generate Cryptographic Keys: A unique public and private key pair must be generated using a secure cryptographic algorithm (e.g., RSA, ECDSA). The private key will be used to sign the request payload, and the corresponding public key will be used by the system to verify the signature.
2. Upload Public Key: The generated public key must be securely uploaded to the merchant preferences within the dashboard. This allows the system to associate the public key with the account for signature verification. Assistance with this step can be requested from the support team if necessary.
## Sandbox Environment Integration
During development and testing in the sandbox environment, implementation of the payload signature can begin:
1. Construct the X-Signature Header: For each API request made with the mode=sandbox parameter, an X-Signature header can be included in the request.
2. Generate the Signature: The value of the X-Signature header should be the cryptographic signature of the JSON request body, generated using the private key. The specific signing process will depend on the chosen cryptographic algorithm.
3. Signature Verification (Sandbox): When a request with the mode=sandbox parameter includes the X-Signature header, the system will attempt to verify the signature using the public key associated with the account.
* If the signature is valid, the request will be processed normally.
* If the signature is invalid or malformed, the API will return an error indicating a signature verification failure.
* Note: If the X-Signature header is not present in sandbox mode requests, it will be ignored, and the request will proceed without signature verification.
## Live Environment Enforcement
To enforce payload signature verification for live transactions (mode=live), an additional step is required:
* Enable "Force Signature" on Dashboard: Within the merchant dashboard, the option to "force traffic to use signature" can be enabled by ticking the corresponding checkbox.
* Signature Verification (Live): Once this option is enabled:
* All subsequent API requests with mode=live must include a valid X-Signature header.
* If a request in mode=live is missing the X-Signature header or contains an invalid signature, the API will return an error indicating a signature verification failure.
* Note: Before enabling this option in the live environment, ensure that signature generation and handling are correctly implemented and thoroughly tested in the sandbox environment.
By following these steps, the payload signature feature can be effectively integrated and utilized to enhance the security of transactions. Secure management of the private key and keeping the public key updated in the dashboard are essential.
# Account Provisioning
Source: https://docs.hub2.io/integration/en/other_products/provisioning
This documentation was last updated on 07/11/23.
## Introduction
Account provisioning is a type of transaction that allows funds to be transferred from the collection account (PAY-IN account) to the transfer account (PAY-OUT account) of the same merchant.
This action is subject to validation and approval by the platform administrators. However, for amounts under a threshold, the platform may decide to automatically approve it.
Please contact the support team for more information on the provisioning conditions.
## Create a Provisioning
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/provisionings' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": 200,
"currency": "XOF",
"description": "TEST APPRO",
}'
```
```typescript Typescript theme={null}
const headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
const data = {
amount: 200,
currency: "XOF",
description: "TEST APPRO",
}
fetch('https://api.hub2.io/provisionings', {
method: 'POST',
headers: headers,
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => console.log(data))
.catch((error) => console.error('Error:', error));
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"amount": 200,
"currency": "XOF",
"description": "TEST APPRO",
}
response = requests.post('https://api.hub2.io/provisionings', headers=headers, json=data)
print(response.json())
```
The response to this command is an action that has been created and is awaiting approval.
```json theme={null}
{
"id": 32,
"action": "provisioning",
"status": "created",
"authorId": "apiKey",
"merchantId": "[REDACTED]",
"params": {
"amount": 200,
"currency": "XOF",
"description": "TEST APPRO",
"mode": "sandbox"
},
"createdAt": "2023-11-07T11:21:19.561Z",
"updatedAt": "2023-11-07T11:21:19.561Z"
}
```
Once the action is approved, the corresponding transaction can be viewed from the route that provides the list of provisionings or from the dashboard.
## Retrieve Provisionings
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/provisionings' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
const url = 'https://api.hub2.io/provisionings';
const headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
};
fetch(url, { method: 'GET', headers: headers })
.then(response => response.json())
.then(data => console.log(data))
.catch((error) => console.error('Error:', error));
```
```python Python theme={null}
import requests
url = "https://api.hub2.io/provisionings"
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.request("GET", url, headers=headers)
print(response.text)
```
```json theme={null}
{
"data": [
{
"id": "prov_wz17MSJ2vRZcefkAdSehs",
"date": "2023-10-17T12:31:27.793Z",
"withdrawal": {
"id": "wtd_9meMoAxGjGaqfamCb_cEU",
"type": "withdrawal",
"date": "2023-10-17T12:31:27.959Z",
"amount": "100",
"currency": "XOF"
},
"deposit": {
"id": "dep_zLHUdczCwG52EGAWvldFf",
"type": "deposit",
"date": "2023-10-17T12:31:27.959Z",
"amount": "100",
"currency": "XOF"
},
"description": "TEST OL PREPROD",
"status": "successful"
},
{
"id": "prov_Mkw33s1i-DIacr6J4CN0C",
"date": "2023-10-17T12:31:27.749Z",
"withdrawal": {
"id": "wtd_OqpCQzO0q84e508CbL0WU",
"type": "withdrawal",
"date": "2023-10-17T12:31:27.831Z",
"amount": "100",
"currency": "XOF"
},
"deposit": {
"id": "dep_KLuaXcEGeor-qC9vvolC2",
"type": "deposit",
"date": "2023-10-17T12:31:27.831Z",
"amount": "100",
"currency": "XOF"
},
"description": "TEST OL PREPROD",
"status": "successful"
},
{
"id": "prov_N1xMSdSAFexce4DbPnNDu",
"date": "2023-10-17T12:31:04.405Z",
"withdrawal": {
"id": "wtd__DjJfMdNgk7XAAkWVtpNW",
"type": "withdrawal",
"date": "2023-10-17T12:31:04.473Z",
"amount": "100",
"currency": "XOF"
},
"deposit": {
"id": "dep_jkUWqh-kMxhkJyvunk9ub",
"type": "deposit",
"date": "2023-10-17T12:31:04.473Z",
"amount": "100",
"currency": "XOF"
},
"description": "TEST OL PREPROD",
"status": "successful"
}
],
"totalItems": 13,
"currentPage": 1,
"countPerPage": 10
}
```
# Public data
Source: https://docs.hub2.io/integration/en/other_products/public_data
# Introduction
These are public endpoints available to all merchants. They provide data on supported countries and providers.
# List of countries
```bash CURL theme={null}
curl --location --request GET 'https://api.hub2.io/data/countries' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```javascript Javascript theme={null}
const url = 'https://api.hub2.io/data/countries';
const headers = {
'Environment': 'sandbox',
'Content-Type': 'application/json'
};
fetch(url, {
method: 'GET',
headers: headers
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There was a problem with your fetch operation:', error);
});
```
```python Python theme={null}
import requests
url = 'https://api.hub2.io/data/countries'
headers = {
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(response.json())
else:
print('Error:', response.status_code)
```
```java Java theme={null}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class Main {
public static void main(String[] args) {
try {
URL url = new URL("https://api.hub2.io/data/countries");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Environment", "sandbox");
connection.setRequestProperty("Content-Type", "application/json");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println(response.toString());
} else {
System.out.println("Error: " + responseCode);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
Please check the endpoint details in the [API reference](/api-reference/data/get-countries) for full details.
The country code (BF, SN, etc.) should be used in the destination fields for payments and transfers.
# List of providers
```bash CURL theme={null}
curl --location --request GET 'https://api.hub2.io/data/providers' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```javascript Javascript theme={null}
const url = 'https://api.hub2.io/data/providers';
const headers = {
'Environment': 'sandbox',
'Content-Type': 'application/json'
};
fetch(url, {
method: 'GET',
headers: headers
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('There was a problem with your fetch operation:', error);
});
```
```python Python theme={null}
import requests
url = 'https://api.hub2.io/data/providers'
headers = {
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(response.json())
else:
print('Error:', response.status_code)
```
```java Java theme={null}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class Main {
public static void main(String[] args) {
try {
URL url = new URL("https://api.hub2.io/data/providers");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Environment", "sandbox");
connection.setRequestProperty("Content-Type", "application/json");
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println(response.toString());
} else {
System.out.println("Error: " + responseCode);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
Please check the endpoint details in the [API reference](/api-reference/data/get-providers) for full details.
The name of the provider should be used in the destination fields for payments and transfers. This field is case-insensitive.
# Recipients
Source: https://docs.hub2.io/integration/en/other_products/recipients
## Introduction
This feature is designed for merchants who want to transfer money using IRT transfers. It enables the management of recipients efficiently.
## Recipient Management
A recipient is an individual who receives funds from an international transfer via mobile money or bank transfer.
The available actions for recipients include:
* Creation – Adding a new recipient
* Retrieval – Viewing a list of recipients or a specific recipient
* Deletion – Removing a recipient
For complete details, refer to the [API reference](/api-reference/recipients).
## Account Verification
When a new recipient is created, HUB2 verifies the existence of their account based on:
* Phone number for mobile money
* IBAN for bank transfers
Once verified, KYC (Know Your Customer) information is recorded to confirm the recipient's legitimacy. This ensures that merchants can securely transfer funds to the intended recipient.
HUB2 also provides an endpoint to retrieve KYC information. See: [KYC Endpoint](/api-reference/compliance/read-a-kyc).
## Next Steps
Once registered, each recipient is assigned a unique internal identifier. Merchants can use this identifier to initiate IRT transfers via the following endpoint:
[Make IRT transfer with verified recipient](/api-reference/irt/create-irt-transfer-verified-recipient)
# Refunds
Source: https://docs.hub2.io/integration/en/other_products/refunds
# Lifecycle
To help getting started with the Refunds API, this illustration shows off how refunds are handled by HUB2 API.
# API Reference
The *Refunds API* allows merchants to initiate refunds for previously successful transfers.
* [Refunds](#refunds)
* [Creating a refund](#creating-a-refund)
* [Restrictions](#restrictions)
* [Retrieving refund details](#retrieving-refund-details)
* [List refunds](#list-refunds)
* [Pagination](#pagination)
***
## Creating a refund
[HUB2 API reference - Create a refund](/api-reference/refunds/create-refund)
Creating a refund requires one call to the dedicated endpoint with the transfer ID that you want to refund.
Sample request :
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/refunds' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function createRefund() {
const response = await fetch('https://api.hub2.io/refunds', {
method: 'POST',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
createRefund();
```
```python Python theme={null}
import json
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
}
response = requests.post(
'https://api.hub2.io/refunds',
headers=headers,
data=json.dumps(data)
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
This request will initiate a refund for the specified transfer. The refund will be processed asynchronously and you will receive webhooks notifications about the status changes.
For any ticket to HUB2 support team concerning a refund, the refund ID will be asked, not the transfer ID.
### Restrictions
* Only successful transfers can be refunded
* A transfer can only be refunded once
* The refund amount will be the full amount of the original transfer
***
## Retrieving refund details
[HUB2 API reference - Get refund details](/api-reference/refunds/retrieve-a-refund)
To retrieve a refund, perform a call to the dedicated endpoint. *Note : This endpoint requires a refund ID, obtained when creating a refund ([see previous step](#creating-a-refund)).*
This endpoint was designed to get the full details of a refund, including its current status and processing information.
***This endpoint***, like every endpoint on HUB2 API, ***is rate limited***. That means that if this endpoint is called too many times by a merchant ID, the HUB2 API will respond with `HTTP 429 Too Many Requests` to that merchant ID.
Documentation about rate limits [can be found here](/documentation/en/limits).
Sample request :
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/refunds/123' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getRefund() {
const response = await fetch('https://api.hub2.io/refunds/123', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const refund = await response.json();
console.log(refund);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getRefund();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/refunds/123',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
Once a Refund has been created and webhooks have been configured to receive notifications about the Refund's lifecycle, HUB2 will send webhooks for each event associated with configured webhooks.
**This is the recommended method for taking into account changes in the status of a Refund.** Some Refunds are processed quickly, while others less so, for a variety of unforeseeable reasons. There is no point in retrieving the details of a Refund to retrieve the status of a Refund if no change has taken place. This is why setting up webhooks is recommended.
Documentation about webhooks [can be found here](/integration/en/webhooks/webhooks_overview).
***
## List refunds
[HUB2 API reference - List refunds](/api-reference/refunds/retrieve-refunds-collection)
To fetch a list of refunds, use the dedicated endpoint. Several query parameters (see below) can be applied to filter our results.
Sample request :
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getRefunds() {
const response = await fetch('https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const refunds = await response.json();
console.log(refunds);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getRefunds();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
Please note the following parameters:
* **Parameter `fromDate` :** identifies the starting date of the refunds retrieval range, the value here is `2023-01-01T00:00:00.000Z`
* **Parameter `toDate` :** identifies the ending date of the refunds retrieval range, the value here is `2023-01-01T12:00:00.000Z`
* **Parameters `page` and `perPage` :** To control navigation through the result pages.
By default, when no filter is defined, **the 100 last refunds are returned** by this endpoint, ordered by `created_at`, descending.
**For a big number of refunds, paginated requests will be mandatory to retrieve all refunds from the HUB2 API.**
### Pagination
Pagination is available on that endpoint.
The response headers provide the `Content-Range` header to inform of the total number of results, and the header value is in the format `0-99/2453`.
In this particular case, this header indicates that the API returned the first 100 results out of a total of 2453 - so there are 25 pages of 100 results to fetch to retrieve all refunds corresponding to the initial filter.
A good advice is to set date filters in the initial request, so the number of results remains the same while pagination is running (except when `toDate` is set to a date in the future).
# Integration
Source: https://docs.hub2.io/integration/en/payment_links/payment-links_integration
Learn how to integrate payment links into your application
## API Integration
The **Payment Link** uses HUB2 API endpoints.
The available endpoints are listed in the following documentation:
## Authentication
Your integration uses the same HUB2 API keys for authentication, but the key must have payment creation permissions:
* `Api.payment_create`
* `Api.payment_intent_create`
* `Api.payment_intent_read`
* `Api.payment_intent_auth_create`
* `Api.payment_fees_read`
## API Endpoints
Payment links use creation and read endpoints similar to standard endpoints
### Payment Link
* `POST /payment-links/` - Create a payment link by defining the necessary payment methods.
Payment and payment intent will be handled based on the customer journey.
Here is an example.
```bash Curl {1} theme={null}
curl --location 'https://api.hub2.io/payment-links' \
--header 'environment: sandbox' \
--header 'merchantId: [REDACTED]' \
--header 'Content-Type: application/json' \
--header 'ApiKey: [REDACTED]' \
--data '{
"purchaseReference": "",
"description":"Payment Links are the fastest way to start accepting payments from your customers.",
"amount": 500,
"currency": "XOF",
"type":"single_use",
"expirationDate": "2025-10-28T20:30:00",
"paymentMethods": [
"mobile_money"
],
"country": "CI",
"providers": [
"Orange",
"Wave",
"MTN",
"Moov"
],
"successUrl":"pay.hub2.io/success",
"failureUrl":"pay.hub2.io/failure"
}'
```
* `PATCH /payment-links/{id}/close` - Close a payment link before expiration
* `GET /payment-links` - List created payment links
* `GET /payment-links/{id}` - Retrieve a specific payment link and display associated payments attempts
## Webhooks
This uses the same webhooks as the payment endpoints. If you have already configured webhooks for payments/, no additional steps are required. Otherwise, see:
# Introduction
Source: https://docs.hub2.io/integration/en/payment_links/payment-links_introduction
Discover our **Payment Links** solution that allows you to collect payments through a simple and secure payment link.
* [Link to API reference](/api-reference/payment-links)
* [Link to OpenAPI documentation](https://api.hub2.io/docs-json)
## Overview
The **payment link** was developed to integrate with merchants' business applications and ERPs who want to collect payments quickly through a secure page. By using payment links, you can benefit from over 20 available payment methods and collect payments in multiple currencies.
## How it works
Available via API, merchants will only need an API key to create payment links, collect payments from customers through a secure payment page hosted by HUB2, and track the various payments received.
1. **Create an API key**
2. **Activate payment methods**
3. **Create links via API**
4. **Track payments from the dashboard or through webhooks associated with the payment.**
## Key Advantages
* **Fast Integration**: Payment links allow merchants who want a quick integration to use our APIs to create links to collect payments from their customers.
* **Share across multiple channels**: Secure payment links can be shared by merchants through all communication channels with their customers, whether by email, WhatsApp, or SMS. These links can also be embedded in applications developed by merchants.
## Use Cases
The *Payment link* is ideal for:
* **Travel agencies:** Travel agencies can directly send payment links to their customers to validate or modify their bookings. These links can expire after a delay defined by the merchant, which allows them to secure reservations.
* **Insurance companies:** In case of failed collection, payment links allow collection services to directly collect amounts due from customers.
* **Electronic invoicing:** To collect payments directly online, merchants can directly add a payment link to invoices sent to their customers.
# Configuration
Source: https://docs.hub2.io/integration/en/payment_links/payment-links_setup
Learn how to configure your API for payment links.
## Configuration in the HUB2 Dashboard
To use payment links, you must first ensure you are using an API key with the necessary permissions.
To do this, you can configure your API keys via our dashboard.
Generate a new API key with permissions for **Payment Links**
* Go to **Management** → **Developers** section
* Click on **Create a key**
* Select the following permissions during creation:
* `payment_create` - To create payments through the API
* `payment_intent_read` -
* `payment_intent_auth_create` -
* `payment_fees_read` -
* `payment_intent_create` -
If you are using an existing API key, update its permissions
* Go to **Management** → **Developers** section
* Find your existing API key
* Click on **Edit** and add the following permissions:
* `payment_create` - To create payments through the API
* `payment_intent_read` -
* `payment_intent_auth_create` -
* `payment_fees_read` -
* `payment_intent_create` -
## Supported Providers
All providers are currently supported by payment links.
A payment link is only valid for one country.
To allow collection with providers from different countries, you will need to create payment links per country.
## Get Help
If you encounter issues during configuration, contact our support team with your specific error messages.
# Authentication
Source: https://docs.hub2.io/integration/en/payments/payments_authentication
A payment can be authenticated either using an OTP code or a redirection link.
## Redirection link
The following providers allows redirection link authentication method:
* Wave
* Ecobank CI
* Orange CI/SN
To retrieve the redirection URLs, please check in the `nextAction` object, that would look like that:
```json theme={null}
{
"nextAction": {
"type": "redirection",
"message": "mm_wave_live.redirection",
"data": {
"url": "https://pay.wave.com/c/wrongUrlForDemo",
"method": "get",
"headers": {},
"data": {}
}
}
}
```
All the necessary information to perform the redirection properly is located in the `nextAction.data` field.
In this example, that would be a request to `GET https://pay.wave.com/c/wrongUrlForDemo`.
HUB2 will return the `Payment` object and will update the status after contacting the provider.
## Authenticate the payment using OTP
For `nextAction` of type `otp`, payment authentication will be required using the dedicated endpoint and the confirmation code `confirmationCode` provided by the end customer.
[Authenticate payment](/api-reference/payments/authenticate-the-current-payment)
**Note:** This request can also be sent directly from client side using the JWT `token`.
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/authentication' \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"confirmationCode": "4567"
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function postAuthentication() {
const response = await fetch('https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/authentication', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"confirmationCode": "4567"
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postAuthentication();
```
```python Python theme={null}
import requests
import json
url = 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/authentication'
headers = {
'Content-Type': 'application/json',
}
data = {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"confirmationCode": "4567"
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
### Orange CI & OTP
When attempting a payment using Orange CI and method OTP, Orange will have a **10 minutes timeout**.
The payment will be expired after that timeout, which can lead to poor user experience for the clients.
Good practice here to avoid that case is to ask the OTP to the client **first**, before attempting the payment.
Once the client provides the OTP, call our endpoint to attempt a payment, directly providing the OTP.
```bash theme={null}
curl --location --request POST 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments' \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001",
"otp": "1234"
}
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function postPayments() {
const response = await fetch('https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001",
"otp": "1234"
}
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postPayments();
```
```python Python theme={null}
import requests
import json
url = 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments'
headers = {
'Content-Type': 'application/json',
}
data = {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001",
"otp": "1234"
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
HUB2 will reach Orange with all the information and they will process the payment immediately.
# Object samples
Source: https://docs.hub2.io/integration/en/payments/payments_examples
### Authentication Successful example
```json theme={null}
{
"id": "pi_tffiDaXyWQu203rIXhujW",
"createdAt": "2022-07-18T06:35:25.932Z",
"updatedAt": "2022-07-18T07:20:05.765Z",
"merchantId": "XXXXX",
"purchaseReference": "Test_YYYY_MM_DD_01",
"customerReference": "Test_01",
"amount": 200,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"status": "successful",
"payments": [
{
"id": "pay_GBc53h6dHvuuq4vlcP6dY",
"intentId": "pi_tffiDaXyWQu203rIXhujW",
"createdAt": "2022-07-18T06:39:20.721Z",
"updatedAt": "2022-07-18T07:20:05.765Z",
"amount": 207,
"currency": "XOF",
"status": "successful",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_bDf6JT6q2JdDOkM153e1A",
"label": "payments.payment_processor_fee",
"rate": 3,
"rateType": "percent",
"amount": 7
}
]
}
],
"mode": "sandbox"
}
```
### Some payments failed and one succeeded example
The `Payments` objects in the `payments` array are ***not chronologically ordered***.
```json theme={null}
{
"id": "pi_-SYVsnP7J35bhAFna5lT9",
"createdAt": "2022-07-18T07:29:57.209Z",
"updatedAt": "2022-07-18T07:39:39.453Z",
"merchantId": "XXXXX",
"purchaseReference": "df_test_2022_07_18_02",
"customerReference": "df_test_2022",
"amount": 200,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpXy1TWVZzblA3SjM1YmhBRm5hNWxUOSIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjkzOTd9.hLCH3W_1QdLD8xKzwvteo82Zhx2pxdo2Y8Mg3MT82L8",
"status": "successful",
"payments": [
{
"id": "pay_TFdgUPOtJUk8AFQm4uKR0",
"intentId": "pi_-SYVsnP7J35bhAFna5lT9",
"createdAt": "2022-07-18T07:30:18.906Z",
"updatedAt": "2022-07-18T07:39:39.453Z",
"amount": 207,
"currency": "XOF",
"status": "failed",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000100",
"fees": [
{
"currency": "XOF",
"id": "fee_kk7BYFirka7FrHg3N6Kdj",
"label": "payments.payment_processor_fee",
"rate": 3,
"rateType": "percent",
"amount": 7
}
],
"failure": {
"code": "authentication_failed",
"message": "La validation du paiement par la client a échoué, le paiement a été annulé."
}
},
{
"id": "pay_3Ar1m6sEb8ikLvSuI5oyh",
"intentId": "pi_-SYVsnP7J35bhAFna5lT9",
"createdAt": "2022-07-18T07:39:31.837Z",
"updatedAt": "2022-07-18T07:39:39.453Z",
"amount": 207,
"currency": "XOF",
"status": "successful",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_ck0eYEbWAB4ONdIqxWhnF",
"label": "payments.payment_processor_fee",
"rate": 3,
"rateType": "percent",
"amount": 7
}
]
},
{
"id": "pay_pbouB8iyXvmvj1UuLuiua",
"intentId": "pi_-SYVsnP7J35bhAFna5lT9",
"createdAt": "2022-07-18T07:32:57.096Z",
"updatedAt": "2022-07-18T07:39:39.453Z",
"amount": 207,
"currency": "XOF",
"status": "failed",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000100",
"fees": [
{
"currency": "XOF",
"id": "fee_PKZjBxVFqu3F97pDFiIVq",
"label": "payments.payment_processor_fee",
"rate": 3,
"rateType": "percent",
"amount": 7
}
],
"failure": {
"code": "authentication_failed",
"message": "La validation du paiement par la client a échoué, le paiement a été annulé."
}
},
{
"id": "pay_C4QiTpenPNQwuT0aTpDDz",
"intentId": "pi_-SYVsnP7J35bhAFna5lT9",
"createdAt": "2022-07-18T07:33:21.932Z",
"updatedAt": "2022-07-18T07:39:39.453Z",
"amount": 207,
"currency": "XOF",
"status": "failed",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000100",
"fees": [
{
"currency": "XOF",
"id": "fee_wEfL3AzP3fU2CGrCs1hjh",
"label": "payments.payment_processor_fee",
"rate": 3,
"rateType": "percent",
"amount": 7
}
],
"failure": {
"code": "authentication_failed",
"message": "La validation du paiement par la client a échoué, le paiement a été annulé."
}
}
],
"lastPaymentFailure": {
"code": "authentication_failed",
"message": "La validation du paiement par la client a échoué, le paiement a été annulé."
},
"mode": "sandbox"
}
```
# Failure causes
Source: https://docs.hub2.io/integration/en/payments/payments_failure_causes
Whenever a payment fails internally, the failure cause is added to the `payment` object returned by the HUB2 API.
**These are "application errors" and not related to the HTTP protocol.**
The HTTP status codes returned by each HUB2 API endpoint are given in the [HUB2 API reference documentation](/api-reference).
The failure causes shown on this page only concern errors encountered during the processing of a transaction.
List of different failure causes with their associated code and descriptions :
| Code | Description |
| :---------------------------- | :---------------------------------------------------------------------------------- |
| `internal_error` | Payment failed. Internal error. Our technical team has been notified. |
| `service_unavailable` | Payment failed. Provider is currently unavailable. |
| `unknown_reason` | Payment failed. No reason was provided by the provider. |
| `unknown_reason_orange` | Payment failed. No reason was given by Orange. |
| `customer_account_locked` | Payment failed. The customer account is locked by provider. |
| `customer_insufficient_funds` | Payment failed. The customer doesn't have enough funds. |
| `authentication_failed` | Payment failed. The customer failed to authenticate the payment. |
| `authentication_timeout` | Payment failed. The customer did not authenticate the payment in time. |
| `timeout` | Payment failed. The waiting period for payment execution has expired. |
| `bad_parameters` | Payment failed. Bad parameters. |
| `forbidden_by_provider` | Payment failed. The payment was forbidden by the provider. |
| `too_many_request` | Payment failed. Provider is overloaded. Please try again later. |
| `duplicate_request` | Payment failed. A similar operation has just been recorded. Please try again later. |
| `canceled_by_customer` | Payment failed. Canceled by the customer. |
| `canceled` | Payment failed. Canceled by the provider. |
| `fraud_suspicion` | Payment failed. Fraud suspicion detected by the provider. |
| `unsupported_currency` | Payment failed. This currency is not supported. |
| `payer_quota_exceeded` | Payment failed. Customer quota exceeded. |
| `invalid_payment_processor` | Payment failed. The selected provider does not allow to perform this operation. |
| `invalid_msisdn` | Payment failed. Incorrect phone number (MSISDN). |
| `wave_payment_expired` | Payment expired. Do not use the corresponding Wave's payment link anymore. |
| `blacklisted_msisdn` | The MSISDN is temporarily banned. |
| `card_expired` | Payment failed. The card's expiry date has passed. |
| `card_declined` | Payment failed. The card has been declined. |
| `card_stolen` | Payment failed. The card is reported as stolen. |
| `card_refused` | Payment failed. The card has been declined. |
| `card_invalid` | Payment failed. Invalid Credit Card. |
| `card_cvn_invalid` | Payment failed. The card security key is invalid. |
| `address_verification_failed` | Payment failed. Address does not match credit card. |
| `card_limit_reached` | Payment failed. The card limit has been reached. |
# Integration
Source: https://docs.hub2.io/integration/en/payments/payments_integration
This guide will describe the process, step by step:
1. Create the payment intent `PaymentIntent`
2. Attempt a payment
3. (Optional) On client's side, authenticate the payment attempt
4. HUB2's side: Processing the payment with the providers/payment platforms
5. Fetching the payment intent `PaymentIntent`'s status
Here are the detailed steps on how to proceed with these actions.
## Create a payment intent
The `PaymentIntent` object is used to represent the intent to collect a payment from a customer. It keeps track of fees and various payment attempts throughout the processing.
The `PaymentIntent` must be created on **the merchant server** with an `amount`, a `currency`, your customer reference and your purchase reference.
[Create a payment intent](/api-reference/payments/create-a-paymentintent-object)
Here is a sample of request to that endpoint:
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/payment-intents' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"customerReference": "Test_01",
"purchaseReference": "Test_YYYY_MM_DD_01",
"amount": 200,
"currency": "XOF"
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function postPaymentIntents() {
const response = await fetch('https://api.hub2.io/payment-intents', {
method: 'POST',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"customerReference": "Test_01",
"purchaseReference": "Test_YYYY_MM_DD_01",
"amount": 200,
"currency": "XOF"
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postPaymentIntents();
```
```python Python theme={null}
import requests
import json
url = 'https://api.hub2.io/payment-intents'
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"customerReference": "Test_01",
"purchaseReference": "Test_YYYY_MM_DD_01",
"amount": 200,
"currency": "XOF"
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
**This ID and this token should be saved somewhere in the merchant infrastructure, it will be required for the next steps.**
* The `id` is the unique reference to the `PaymentIntent`, it will be used to attempt payments
* The `token` is a *JSON Web Token* (JWT) that will be used to authenticate the payment request
```json theme={null}
{
"id": "pi_tffiDaXyWQu203rIXhujW",
"createdAt": "2022-07-18T06:35:25.932Z",
"updatedAt": "2022-07-18T06:35:25.944Z",
"merchantId": "[REDACTED]",
"purchaseReference": "Test_YYYY_MM_DD_01",
"customerReference": "Test_01",
"amount": 200,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"status": "payment_required",
"payments": [],
"mode": "sandbox"
}
```
### Restrictions
When creating a payment intent, checks are performed on the field `reference` : no special characters are allowed. Trying to use special characters here will result in a `400 Bad Request` error.
Allowed characters are letters, numbers, hyphen, underscore, dot and space. Here's the list in the regular expression format : `A-Za-z0-9\-_. `.
## Attempt a payment on the Payment Intent
Once the `PaymentIntent` is successfully created, it will be possible to collect payment information with the final customer and attempt a `Payment`.
To attempt a `Payment`, the `paymentMethod` and all required field that are described in the API reference must be specified .
[Attempt a payment](/api-reference/payments/attempt-a-payment-on-a-paymentintent-object)
As this route does not require your private `API_KEY`, the payment request could be made directly on the **client side** (from the client himself), using the previously retrieved JWT `token` to authenticate the request.
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments' \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001"
}
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function postPaymentIntents() {
const response = await fetch('https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001"
}
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postPaymentIntents();
```
```python Python theme={null}
import requests
import json
url = 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments'
headers = {
'Content-Type': 'application/json',
}
data = {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001"
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
If the request is successful, a `HTTP 201` with the full `PaymentIntent` object in the response body will be returned.
Notice that the status is now `processing` and the `payments` array contains the newly created `Payment` attempt.
The payment `id` can now be saved to identify your payment attempt in the list as *more than one payment* may be attempted for **one** `PaymentIntent`.
```json theme={null}
{
"id": "pi_tffiDaXyWQu203rIXhujW",
"createdAt": "2022-07-18T06:35:25.932Z",
"updatedAt": "2022-07-18T06:39:21.471Z",
"merchantId": "XXXXX",
"purchaseReference": "Test_YYYY_MM_DD_01",
"customerReference": "Test_01",
"amount": 200,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"status": "processing",
"payments": [
{
"id": "pay_GBc53h6dHvuuq4vlcP6dY",
"intentId": "pi_tffiDaXyWQu203rIXhujW",
"createdAt": "2022-07-18T06:39:20.721Z",
"updatedAt": "2022-07-18T06:39:21.471Z",
"amount": 207,
"currency": "XOF",
"status": "created",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_drJQNmGYKQAqT7oulY519",
"label": "payments.customer_fee",
"rate": 3,
"rateType": "percent",
"amount": 7
}
]
}
],
"mode": "sandbox"
}
```
## Retrieve Payment status
After attempting the payment, HUB2 will contact the payment provider and update the payment object according to its response.
From this point, the payment status can be retrieved in two different ways.
### 1. Register a Webhook for payment events (recommended method)
Instead of polling `PaymentIntent` for status updates, we recommend to use webhooks.
Please take a look at [Webhooks Integration](/integration/en/webhooks) to see how to implement them.
Register to `payment` or `payment_intents` related events.
By using this method, the merchant server will be notified once the `Payment` or the `PaymentIntent` is updated.
This way, implementing mechanisms to poll the status is not necessary, rate limiting won't be applied and potential latency issues will be avoided.
Also in case of high payment traffic, it will reduce the load on the merchant server and consequently on HUB2 servers.
### 2. Status Polling
Start polling the payment status using the dedicated API endpoint.
[Retrieve the status payment object](/api-reference/payments/retrieve-the-status-payment-object)
To prevent DoS attack, this route is rate limited, a `HTTP 429 - Too Many Requests` will be returned in this case. Please take this in consideration while implementing the check status polling.
Here is a sample of request on that endpoint:
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/payments/pay_GBc53h6dHvuuq4vlcP6dY/status' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getPaymentStatus() {
const response = await fetch('https://api.hub2.io/payments/pay_GBc53h6dHvuuq4vlcP6dY/status', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox'
}
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getPaymentStatus();
```
```python Python theme={null}
import requests
url = 'https://api.hub2.io/payments/pay_GBc53h6dHvuuq4vlcP6dY/status'
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
This will fetch a payment status, but there's another endpoint dedicated on fetching a payment intent status if need be.
[Retrieve a payment intent status](/api-reference/payments/retrieve-a-paymentintent-status)
Polling should be used only if `PaymentIntent`.`status` is `processing` OR `action_required`. Continuously polling `payment_required`, `successful` or `failed` `PaymentIntent` will result in source IP address being throttled or temporarily **banned**.
In most cases, the next `PaymentIntent` status will be `action_required`. This status mean that the payment attempt requires a manual action from the user to *authenticate* or *validate* the payment.
## Handle user action
The `Payment` object will contain a `nextAction` object that will describe the type of the action that will be required from the user.
```json theme={null}
{
"id": "pi_tffiDaXyWQu203rIXhujW",
"...": "...",
"amount": 200,
"currency": "XOF",
"status": "action_required",
"payments": [
{
"id": "pay_GBc53h6dHvuuq4vlcP6dY",
"intentId": "pi_tffiDaXyWQu203rIXhujW",
"amount": 207,
"currency": "XOF",
"status": "pending",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": ["..."],
"nextAction": {
"type": "otp",
"message": "Mode Sandbox. Entrez un numéro à 4 chiffres pour authentifier le paiement."
}
}
],
"nextAction": {
"type": "otp",
"message": "Mode Sandbox. Entrez un numéro à 4 chiffres pour authentifier le paiement."
}
}
```
This is the part we are interested in : `"nextAction": { "type": "otp", "message": "Mode Sandbox. Entrez un numéro à 4 chiffres pour authentifier le paiement." }`.
The `nextAction.message` should now be displayed to the final customer to let him know what he should do. The action indicates the customer to validate the Payment. There are different types for the action :
* A `redirection` action type provides all the information to redirect the customer to an external page.
* A `ussd` action type indicates to the user the USSD procedure to follow to validate the payment.
* A `otp` action type indicates to the final customer how to generate an One Time Password (OTP). This type of action requires the client to enter the OTP code into a field that will be sent to the [dedicated API Route](/api-reference/payments/authenticate-the-current-payment). Please note that some providers allow to pass the OTP code directly in the payment attempt request (see `otp` field `mobileMoney` object).
## Bank Collection
The "Bank Collection" feature allows a merchant to offer their customers a bank transfer payment option. The customer receives a unique reference to make the transfer.
### Prerequisites
To use this payment method, it is necessary to have completed a KYB (Know Your Business) registration beforehand.
[Create a KYB registration](/api-reference/identity/create-a-kyb-object)
### Payment Process
1. The merchant creates a payment intent (`PaymentIntent`) as described previously
2. For the payment attempt, the merchant specifies `bank_transfer` as the payment method
3. The system generates a unique reference for the transfer
4. The customer receives this reference and can make the transfer
5. The payment status is updated once the transfer is received (automatic or manual update)
[Attempt a payment](/api-reference/payments/attempt-a-payment-on-a-paymentintent-object)
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments' \
--header 'Content-Type: application/json' \
--data-raw '{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "bank_transfer",
"country": "CI",
"provider": "Bank",
"bankTransfer": {
"kybName": "Name declared in KYB",
"expirationDelay": 3600
}
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function postPaymentIntents() {
const response = await fetch('https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "bank_transfer",
"country": "CI",
"provider": "Bank",
"bankTransfer": {
"kybName": "Name declared in KYB",
"expirationDelay": 3600
}
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postPaymentIntents();
```
```python Python theme={null}
import requests
import json
url = 'https://api.hub2.io/payment-intents/pi_tffiDaXyWQu203rIXhujW/payments'
headers = {
'Content-Type': 'application/json',
}
data = {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX3RmZmlEYVh5V1F1MjAzcklYaHVqVyIsIm1lcmNoYW50SWQiOiIzIiwibW9kZSI6InNhbmRib3giLCJpYXQiOjE2NTgxMjYxMjV9.1eB6ifC2ldfRw5UstnVa-bQqIdx9_IGLRdwWyXzZR4o",
"paymentMethod": "bank_transfer",
"country": "CI",
"provider": "Bank",
"bankTransfer": {
"kybName": "Name declared in KYB",
"expirationDelay": 3600
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
### API Response
Upon success, the API will return a `PaymentIntent` object containing the bank transfer payment details, including the unique reference generated for the bank transfer.
**Important**: The reference must be communicated to the end customer so they can make the bank transfer. The payment status will be updated once it is received and processed.
# Introduction
Source: https://docs.hub2.io/integration/en/payments/payments_introduction
The implementation of the HUB2 Payment API is a bit more challenging than the Transfer API, as an interaction with the end customer is required.
* [Link to the API reference for payments](/api-reference/payments)
* [Link to the OpenAPI documentation](https://api.hub2.io/docs-json)
## Payment lifecycle (asynchronous circuit)
This is a common way of how payments are handled by the HUB2 API - it may vary depending on scenarios and providers.
## Payment lifecycle (synchronous circuit)
This way of processing payments is only available for the provider wave.
It allows merchants to receive redirection links without subscribing to a webhook and without doing polling.
This circuit can only be activated on request to HUB2 Support. Please contact HUB2 Support for more information.
**Note :**
* PI : Payment Intent - the payment intent
* P : Payment - the payment attempt
# List payments
Source: https://docs.hub2.io/integration/en/payments/payments_list
Retrieving lists of payments can be done using the dedicated endpoint.
[List payments](/api-reference/payments/retrieve-a-payments-collection)
## Request
Here is a sample of a request to that endpoint:
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/payments?from=2023-01-01T00:00:00.000Z&to=2023-01-01T12:00:00.000Z&page=1&perPage=100' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getPayments() {
const response = await fetch('https://api.hub2.io/payments?from=2023-01-01T00:00:00.000Z&to=2023-01-01T12:00:00.000Z&page=1&perPage=100', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getPayments();
```
```python Python theme={null}
import requests
url = 'https://api.hub2.io/payments?from=2023-01-01T00:00:00.000Z&to=2023-01-01T12:00:00.000Z&page=1&perPage=100'
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
Please note the following parameters:
* Parameter `from` : identify the starting date of the range for wish to fetch payments, the value here is `2023-01-01T00:00:00.000Z`
* Parameter `to` : identify the ending date of the range for wish to fetch payments, the value here is `2023-01-01T12:00:00.000Z`
* Parameters `page` and `perPage` : allow you to navigate in the result pages
**In case of high traffic, it cannot be expected to retrieve all payments without using pagination.**
### Pagination
Pagination is available on that endpoint.
A header is provided in the response to identify the number of results., it's the `Content-Range` header and its content looks like `0-99/2453`.
In that particular case, the first 100 results are fetched on a 2453 total - there is 25 pages of 100 results available.
Don't forget to use date filters too to avoid the number of results changing between two requests.
## Response
Results are automatically ordered by the created date in a descending order.
Here is a sample of the response in JSON format:
```json theme={null}
[
{
"id": "pi_HVSUvR5OvvyZn4ozGonNs",
"createdAt": "2023-11-06T11:52:18.139Z",
"updatedAt": "2023-11-06T11:52:26.382Z",
"merchantId": "wP8PsNc3Je3Djp2FTiQTO",
"purchaseReference": "purchaseReference",
"customerReference": "customerReference",
"amount": 10,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX0hWU1V2UjVPdnZ5Wm40b3pHb25OcyIsIm1lcmNoYW50SWQiOiJ3UDhQc05jM0plM0RqcDJGVGlRVE8iLCJtb2RlIjoic2FuZGJveCIsImlhdCI6MTY5OTI3MTUzOH0.Bg2PIujWvtGIlH1jSPRndUiyBD8WBNRuWUQ242GHCZ8",
"status": "successful",
"payments": [
{
"id": "pay_vhmS6bGBsTCP08rXOAFZJ",
"intentId": "pi_HVSUvR5OvvyZn4ozGonNs",
"createdAt": "2023-11-06T11:52:18.891Z",
"updatedAt": "2023-11-06T11:52:26.402Z",
"amount": 10,
"currency": "XOF",
"status": "successful",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": []
}
],
"mode": "sandbox"
},
{
"id": "pi_-RSGf4F7vqyPxfv2fYGpp",
"createdAt": "2023-11-06T11:52:17.914Z",
"updatedAt": "2023-11-06T11:52:25.978Z",
"merchantId": "wP8PsNc3Je3Djp2FTiQTO",
"purchaseReference": "purchaseReference",
"customerReference": "customerReference",
"amount": 10,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpXy1SU0dmNEY3dnF5UHhmdjJmWUdwcCIsIm1lcmNoYW50SWQiOiJ3UDhQc05jM0plM0RqcDJGVGlRVE8iLCJtb2RlIjoic2FuZGJveCIsImlhdCI6MTY5OTI3MTUzN30.4XX1HeDk_Be_sEvaBcqx6GfxBB2qrFJu7btHVUyLZII",
"status": "successful",
"payments": [
{
"id": "pay_uEBdIHJLwytg6phBvhg3d",
"intentId": "pi_-RSGf4F7vqyPxfv2fYGpp",
"createdAt": "2023-11-06T11:52:18.605Z",
"updatedAt": "2023-11-06T11:52:25.986Z",
"amount": 10,
"currency": "XOF",
"status": "successful",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": []
}
],
"mode": "sandbox"
},
{
"id": "pi_UcRWisVdLCCw2Bq5S7XRl",
"createdAt": "2023-11-06T11:52:17.677Z",
"updatedAt": "2023-11-06T11:52:25.665Z",
"merchantId": "wP8PsNc3Je3Djp2FTiQTO",
"purchaseReference": "purchaseReference",
"customerReference": "customerReference",
"amount": 10,
"currency": "XOF",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlbnRJZCI6InBpX1VjUldpc1ZkTENDdzJCcTVTN1hSbCIsIm1lcmNoYW50SWQiOiJ3UDhQc05jM0plM0RqcDJGVGlRVE8iLCJtb2RlIjoic2FuZGJveCIsImlhdCI6MTY5OTI3MTUzN30.zOPnbV8pRL0LVWPtU_m2bdW5-KIKBkTfffGbiEOvhKY",
"status": "successful",
"payments": [
{
"id": "pay_GdhjiFTsMcT4aeXrmK6xJ",
"intentId": "pi_UcRWisVdLCCw2Bq5S7XRl",
"createdAt": "2023-11-06T11:52:18.352Z",
"updatedAt": "2023-11-06T11:52:25.668Z",
"amount": 10,
"currency": "XOF",
"status": "successful",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": []
}
],
"mode": "sandbox"
}
]
```
# Sandbox MSISDN
Source: https://docs.hub2.io/integration/en/payments/payments_sandbox_msisdn
In sandbox mode, several MSISDN are available to ease integration:
| MSISDN | Code | |
| :------: | :---------------------- | :---------------------------- |
| 00000001 | successful | |
| 00000002 | successful (with delay) | |
| 00000003 | pending (blocked) | |
| 00000100 | failed | `authentication_failed` |
| 00000101 | failed | `authentication_timeout` |
| 00000200 | failed | `unknown_reason` |
| 00000201 | failed | `customer_insufficient_funds` |
| 00000202 | failed | `customer_account_locked` |
| 00000203 | failed | `forbidden_by_provider` |
| 00000204 | failed | `bad_parameters` |
| 00000205 | failed | `payer_quota_exceeded` |
| 00000206 | failed | `invalid_amount` |
| 00000211 | failed | `canceled_by_customer` |
| 00000300 | failed | `timeout` |
| 00000301 | failed | `too_many_request` |
| 00000302 | failed | `service_unavailable` |
| 00000303 | failed | `internal_error` |
| 00000304 | failed | `wave_payment_expired` |
| 00000305 | failed | `duplicate_request` |
| 00000400 | failed | `invalid_msisdn` |
Below is the list of MSISDN available per gateway:
| Connector | 00000001 | 00000002 | 00000003 | 00000100 | 00000101 | 00000200 | 00000201 | 00000202 | 00000203 | 00000204 | 00000205 | 00000206 | 00000211 | 00000300 | 00000301 | 00000302 | 00000303 | 00000304 | 00000305 | 00000400 |
| --------------- | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: |
| mm\_bf\_moov | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| mm\_bj\_moov | ✔️ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ |
| mm\_bj\_celtiis | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| mm\_\*\_wave | ✔️ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ✔️ |
| mm\_ci\_moov | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ |
| mm\_ci\_mtn | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ |
| mm\_ci\_orange | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ |
| mm\_sn\_free | ✔️ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ✔️ |
| mm\_sn\_orange | ✔️ | ❌ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ |
| mm\_tg\_moov | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ✔️ | ❌ |
\*: within the limits of available countries and providers
# Integration
Source: https://docs.hub2.io/integration/en/processing_only/processing_only_integration
Learn how to integrate Processing Only mode into your application
## API Integration
**Processing Only** mode uses dedicated HUB2 API endpoints that are different from our traditional aggregation model. The main difference is that transactions are processed directly through your provider accounts instead of our aggregated accounts.
Available endpoint are availble at:
## Authentication
Your integration uses the same HUB2 API keys for authentication but the key must have new permissions:
* `Api.delegated_payment_create`
* `Api.delegated_transfer_create`
## Transaction Flow
### 1. Transaction Request
When you initiate a transaction, HUB2 will:
1. **Validate Request**: Check your API key and request format
2. **Route to Provider**: Direct the request to your configured provider account
3. **Process Transaction**: Execute the transaction using your provider credentials
4. **Return Response**: Provide a unified response format regardless of the provider
### 2. Provider Processing
The actual transaction processing happens directly with your provider:
```mermaid theme={null}
sequenceDiagram
participant M as Merchant
participant H as HUB2
participant P as Provider
M->>H: Payment Request
H->>P: Process via Merchant Account
P->>H: Transaction Response
H->>M: Unified Response
```
## API Endpoints
Processing Only mode uses dedicated creation endpoints, while reading endpoints remain the same as traditional ones:
### Payments
* `POST /delegated/payments` - Create a payment using your provider account
Unlike classic payments, there's no need to create a payment intent first before attempting a payment.
Here, you directly make the payment attempt. Here's an example.
```bash Curl {1} theme={null}
curl --location 'https://api.hub2.io/delegated/payments' \
--header 'environment: sandbox' \
--header 'merchantId: [REDACTED]' \
--header 'Content-Type: application/json' \
--header 'ApiKey: [REDACTED]' \
--data '{
"customerReference": "",
"purchaseReference": "",
"amount": 120,
"currency": "XOF",
"paymentMethod": "mobile_money",
"country": "CI",
"provider": "orange",
"mobileMoney": {
"msisdn": "00000001",
"onCancelRedirectionUrl": "https://failed.com",
"onFinishRedirectionUrl": "https://success.com",
"workflow": "redirection"
}
}'
```
* `POST /delegated/payments/sync` - Create a payment using your provider account with synchronous mode (the list of providers supporting synchronous is availabe at [https://docs.hub2.io/integration/en/getting\_started/api\_operation#synchronous-circuit](https://docs.hub2.io/integration/en/getting_started/api_operation#synchronous-circuit))
* `GET /payments` - List payments (same as classic payments)
* `GET /payments/{id}/status` - Get payment status (same as classic payments)
### Transfers
* `POST /delegated/transfers` - Create a transfer using your provider accounts. Below is an example. Only the endpoint changes here in the request.
```bash Curl {1} theme={null}
curl --location --request POST 'https://api.hub2.io/delegated/transfers' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"reference": "",
"amount": 2000,
"currency": "XOF",
"description": "",
"destination": {
"type": "mobile_money",
"country": "CI",
"recipientName": "John Doe",
"msisdn": "+225000000000",
"provider": "orange"
}
}'
```
```typescript Typescript {4} theme={null}
import fetch from 'node-fetch';
async function postTransfer() {
const response = await fetch('https://api.hub2.io/delegated/transfers', {
method: 'POST',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"reference": "",
"amount": 2000,
"currency": "XOF",
"description": "",
"destination": {
"type": "mobile_money",
"country": "CI",
"recipientName": "John Doe",
"msisdn": "+225000000000",
"provider": "orange"
}
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postTransfer();
```
```python Python lines {26} theme={null}
import json
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"reference": "",
"amount": 2000,
"currency": "XOF",
"description": "",
"destination": {
"type": "mobile_money",
"country": "CI",
"recipientName": "John Doe",
"msisdn": "+225000000000",
"provider": "orange"
}
}
response = requests.post(
'https://api.hub2.io/delegated/transfers',
headers=headers,
data=json.dumps(data)
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
* `POST /delegated/transfers/irt` - Create an IRT transfer using your provider accounts
* `GET /transfers` - List transfers (same as classic transfers)
* `GET /transfers/{id}/status` - Get transfer status (same as classic transfers)
## Webhooks
This uses the same webhooks as the classic circuit. If you've already configured webhooks for payments/
transfers, no extra steps are required. Else, see:
# Introduction
Source: https://docs.hub2.io/integration/en/processing_only/processing_only_introduction
Learn about our Processing Only solution that acts as a gateway for merchants using their own provider accounts
* [Link to the API reference for processing only](/api-reference/processing-only)
* [Link to the OpenAPI documentation](https://api.hub2.io/docs-json)
## Overview
Our **Processing Only** solution is designed for merchants who prefer using their own provider accounts while still benefiting from our platform’s unified API and centralized dashboard.
## How It Works
Unlike our traditional aggregation model, the Processing Only solution works as a **gateway**:
1. **Merchant Registration**: Merchants register their own provider account credentials through our dashboard
2. **Direct Provider Connection**: Transactions are processed directly through the merchant's provider accounts
3. **Unified Interface**: Merchants still benefit from our unified API and dashboard for transaction management
## Key Benefits
* **Direct Provider Relationship**: Merchants maintain direct relationships with their chosen providers
* **Unified Management**: Single dashboard and API for managing multiple provider relationships
## Use Cases
Processing Only is ideal for:
* **Established Merchants**: Merchants who already have established relationships with providers
* **Regulatory Requirements**: When direct relationships with providers are mandatory.
* **Risk Management**: When merchants prefer to maintain direct control over their provider relationships
## Next Steps
To get started with Processing Only:
1. [Set up your provider credentials](/integration/en/processing_only/processing_only_setup)
2. [Configure your integration](/integration/en/processing_only/processing_only_integration)
# Setup
Source: https://docs.hub2.io/integration/en/processing_only/processing_only_setup
Learn how to set up your provider credentials for Processing Only mode.
## Configuration in HUB2 dashboard
To use Processing Only mode, you must first ensure that you are using an API key with the necessary permissions. To do this, you can configure your API keys via our dashboard. Once this is done, you can move on to the step of configuring provider credentials.
Before configuring provider credentials, ensure your API key has the necessary permissions for Processing Only mode:
Generate a new API key with Processing Only permissions
* Go to **Management** → **Developers**
* Click **Create a key**
* Select the following permissions during creation:
* `Api.delegated_payment_create` - For creating delegated payments
* `Api.delegated_transfer_create` - For creating delegated transfers
If using an existing API key, update its permissions
* Go to **Management** → **Developers**
* Find your existing API key
* Click **Edit** and add the following permissions:
* `Api.delegated_payment_create` - For creating delegated payments
* `Api.delegated_transfer_create` - For creating delegated transfers
API keys without these permissions won't be able to access endpoints `/delegated/`.
These are the information (API key, username, parameters...) giving you access to the provider's API.
1. Log in to your HUB2 dashboard
2. Navigate to **Management** → **Connectors credentials** (in sidebar menu)
3. Click on **Register new credentials**
4. In the modal, select the transaction type and select the provider in the dropdown list
5. **Enter Credentials**: Provide your provider account credentials
A credentials validation action is systematically performed before registration to confirm that the credentials are correct. This may include:
* Call to the authentication token generation endpoint
* Call to a GET /balance endpoint
If validation fails, an error message will be displayed, prompting you to recheck your input.
## Supported Providers
The list of supported providers becomes available when the dropdown in the modal is expanded.
Below is the list of currently supported Providers.
| Providers | Available Services |
| --------- | -------------------- |
| Orange CI | Payment and Transfer |
| MTN CI | Payment and Transfer |
| Orange SN | Payment and Transfer |
This list will be updated as services are added.
## Best Practices
* **Regular Updates**: Update your credentials when they expire
* **Monitoring**: Regularly monitor your provider account activity
* **Backup Credentials**: Keep backup credentials in case of emergencies
## Getting Help
If you encounter issues during setup, contact our support team with your specific error messages
# Integration
Source: https://docs.hub2.io/integration/en/submerchants/submerchants_integration
Learn how to create submerchants from our API
## API Integration
The **Submerchant** feature uses HUB2 API endpoints.
The available endpoints are listed in the following documentation:
## Authentication
Your integration uses the same HUB2 API keys for authentication, but the key must have payment creation permissions:
* `Api.payment_create`
* `Api.payment_intent_create`
* `Api.payment_intent_read`
* `Api.payment_intent_auth_create`
* `Api.payment_fees_read`
## API Endpoints
Submerchants use creation and read endpoints similar to standard endpoints
### Submerchants
* `POST /submerchants/` - Create a submerchant.
Here is an example.
```bash Curl {1} theme={null}
curl --location 'https://api.hub2.io/submerchants' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: live' \
--header 'Content-Type: application/json' \
--data '{
"name": "EM_TEST_01",
"description": "Submerchant created for testing purposes",
"address": "RIVIERA BEVERLY HILLS",
"zipcode": "00225",
"city": "ABIDJAN",
"phoneNumber": "+2250707070707"
}'
```
* `PATCH /submerchants/{submerchantId}/` - Update a submerchant
* `GET /submerchants` - List created submerchants
* `GET /submerchants/{id}` - Retrieve a specific submerchant
## Webhooks
This uses the same webhooks as the payment endpoints. If you have already configured webhooks for payments/, no additional steps are required. Otherwise, see:
# Introduction
Source: https://docs.hub2.io/integration/en/submerchants/submerchants_introduction
Discover our **submerchants** solution that allows you to onboard a new submerchant and generate their unique ID for payment processing and transfers.
* [Link to API reference](/api-reference/submerchants)
* [Link to OpenAPI documentation](https://api.hub2.io/docs-json)
## Overview
The **submerchant** feature enables a Parent Merchant (Fintech or PSP) to **manage multiple submerchants**. It ensures that transactions are correctly attributed to each submerchant and facilitates granular compliance management.
## How it works
Available via API, merchants will only need an API key to list, create and update submerchants.
1. **Create an API key**
2. **Create submerchant via API**
3. **Add SubmerchantId for Payment and Transfer transactions**
4. **Track payments from the dashboard or through webhooks associated with the payment.**
## Key Advantages
* **Risk Mitigation**: You can block a single submerchant without impacting the others, reducing the risk of an entire platform being blocked due to one bad actor.
* **Granular Control**: You gain the ability to view and follow the status of each submerchant individually.
* **Traceability**: All operations are scoped, and every transaction is tagged with a `sub_merchant_id`, ensuring detailed logs and traceability via APIs and the dashboard.
# Configuration
Source: https://docs.hub2.io/integration/en/submerchants/submerchants_setup
Learn how to configure your API for submerchants.
## Configuration in the HUB2 Dashboard
To use submerchants feature, you must first ensure you are using an API key with the necessary permissions.
To do this, you can configure your API keys via our dashboard.
Generate a new API key with permissions for **submerchants**
* Go to **Management** → **Developers** section
* Click on **Create a key**
* Select the following permissions during creation:
* `submerchant_create` - To create submerchants through the API
* `submerchant_list` -
* `submerchant_update` -
* `submerchant_manage_status` -
If you are using an existing API key, update its permissions
* Go to **Management** → **Developers** section
* Find your existing API key
* Click on **Edit** and add the following permissions:
* `submerchant_create` - To create submerchants through the API
* `submerchant_list` -
* `submerchant_update` -
* `submerchant_manage_status` -
## Get Help
If you encounter issues during configuration, contact our support team with your specific error messages.
# Failure causes
Source: https://docs.hub2.io/integration/en/transfers/transfers_failure_causes
Whenever a transfer fails internally, the failure cause is added to the `transfer` object returned in the HTTP response.
**These are "application errors" and not related to the HTTP protocol.**
The HTTP status codes returned by each HUB2 API endpoint are given in the [HUB2 API reference documentation](/api-reference).
The failure causes shown on this page only concern errors encountered during the processing of a transaction.
List of different failure causes with their associated codes and descriptions :
| Code | Description |
| :-------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
| `internal_error` | Transfer failed. Internal error. Our technical team has been notified. |
| `invalid_amount` | Transfer failed. The amount is invalid. Please [refer to the amount policies documentation](/documentation/en/policies#pay-out). |
| `service_unavailable` | Transfer failed. Provider is currently unavailable. |
| `too_many_request` | Transfer failed. Provider is overloaded. Please try again later. |
| `unknown_reason` | Transfer failed. No reason was provided by the provider. |
| `fraud_suspicion` | Transfer failed. Fraud suspicion detected by the provider. |
| `invalid_destination` | Transfer failed. Destination is not valid. |
| `forbidden_by_provider` | Transfer failed. Refused by provider. |
| `timeout` | Transfer failed. The waiting time with provider is exceeded. |
| `insufficient_funds` | Transfer failed. Merchant doesn't have enough funds. |
| `unsupported_currency` | Transfer failed. This currency is not supported. |
| `canceled` | Transfer failed. Canceled by provider. |
| `destination_not_allowed` | Transfer failed. Transfer to this recipient is forbidden by the provider. |
| `blacklisted_msisdn` | The MSISDN is temporarily banned. |
| `invalid_payment_processor` | Transfer failed. The selected provider does not support this operation. |
| `duplicate_request` | Transfer failed. A similar operation has just been recorded. Please try again later. |
| `bad_request` | Transfer failed. The received request contains errors or is incorrectly formatted. |
# Integration
Source: https://docs.hub2.io/integration/en/transfers/transfers_integration
# Transfers (Pay-Outs)
The *Transfer API (Pay-Out API)* is quite simple to implement, as there is no interaction with end customers.
* [Transfers (Pay-Outs)](#transfers-pay-outs)
* [Making a transfer](#making-a-transfer)
* [Restrictions](#restrictions)
* [Retrieving transfer details](#retrieving-transfer-details)
* [Retrieving transfer status](#retrieve-a-transfer-status)
* [List transfers](#list-transfers)
* [Pagination](#pagination)
***
## Making a transfer
[HUB2 API reference - Create a transfer](/api-reference/transfers/create-transfer)
Performing a transfer only requires one call to the dedicated endpoint.
Sample request :
```bash Curl theme={null}
curl --location --request POST 'https://api.hub2.io/transfers' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"reference": "",
"amount": 2000,
"currency": "XOF",
"description": "",
"destination": {
"type": "mobile_money",
"country": "CI",
"recipientName": "John Doe",
"msisdn": "+225000000000",
"provider": "orange"
}
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function postTransfer() {
const response = await fetch('https://api.hub2.io/transfers', {
method: 'POST',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"reference": "",
"amount": 2000,
"currency": "XOF",
"description": "",
"destination": {
"type": "mobile_money",
"country": "CI",
"recipientName": "John Doe",
"msisdn": "+225000000000",
"provider": "orange"
}
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
postTransfer();
```
```python Python theme={null}
import json
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"reference": "",
"amount": 2000,
"currency": "XOF",
"description": "",
"destination": {
"type": "mobile_money",
"country": "CI",
"recipientName": "John Doe",
"msisdn": "+225000000000",
"provider": "orange"
}
}
response = requests.post(
'https://api.hub2.io/transfers',
headers=headers,
data=json.dumps(data)
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
This request will return with a transfer ID, which is the unique identifier of the transfer. An unique ID is created on the HUB2 platform for each transfer request.
**Transfer ID must be saved in the merchant's database for the next steps.**
For any ticket to HUB2 support team concerning a transfer, the transfer ID will be asked, not the merchant's reference.
### Restrictions
When creating a transfer, checks are performed on the field `reference` : no special characters are allowed. Trying to use special characters here will result in a `400 Bad Request` error.
Allowed characters are letters, numbers, hyphen, underscore, dot and space. Here's the list in the regular expression format : `A-Za-z0-9\-_. `.
***
## Retrieving transfer details
[HUB2 API reference - Get transfer details](/api-reference/transfers/retrieve-a-transfer)
To retrieve a transfer, perform a call to the dedicated endpoint. *Note : This endpoint requires a transfer ID, obtained when making a transfer ([see previous step](#making-a-transfer)).*
This endpoint was designed more to get the full details of a transfer, *rather than to be only used to check for transfer status changes*.
***This endpoint***, like every endpoint on HUB2 API, ***is rate limited***. That means that if this endpoint is called too many times by a merchant ID, the HUB2 API will respond with `HTTP 429 Too Many Requests` to that merchant ID.
Documentation about rate limits [can be found here](/documentation/en/limits).
Sample request :
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/transfers/tr_xxxxxxxxxxxxxxxxxx' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getTransfer() {
const response = await fetch('https://api.hub2.io/transfers/tr_xxxxxxxxxxxxxxxxxx', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const transfer = await response.json();
console.log(transfer);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getTransfer();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/transfers/tr_xxxxxxxxxxxxxxxxxx',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
Once a Transfer has been created and webhooks have been configured to receive notifications about the Transfer's lifecycle, HUB2 will send webhooks for each event associated with configured webhooks.
**This is the recommended method for taking into account changes in the status of a Transfer.** Some Transfers are processed quickly, while others less so, for a variety of unforeseeable reasons. There is no point in retrieving the details of a Transfer to retrieve the status of a Transfer if no change has taken place. This is why setting up webhooks is recommended.
Documentation about webhooks [can be found here](/integration/en/webhooks/webhooks_overview).
***
## Retrieve a transfer status
This feature was added February the 4th of 2025.
As the previous endpoint was not meant to retrieve a transfer status because of performance reasons, a new dedicated endpoint was added specifically for that purpose.
[HUB2 API reference - Retrieve a transfer status](/api-reference/transfers/retrieve-a-transfers-status)
This endpoint is still rate limited but the rate limit settings will be lower than the one above.
Sample request :
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/transfers/tr_xxxxxxxxxxxxxxxxxx/status' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getTransfer() {
const response = await fetch('https://api.hub2.io/transfers/tr_xxxxxxxxxxxxxxxxxx/status', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const transfer = await response.json();
console.log(transfer);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getTransfer();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/transfers/tr_xxxxxxxxxxxxxxxxxx/status',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
*If any trouble is encountered while implementing the webhooks mechanism, this endpoint could be used to fetch the status of a transfer instead.*
***
## List transfers
[HUB2 API reference - List transfers](/api-reference/transfers/retrieve-a-transfer-collection)
To fetch a list of transfers, use the dedicated endpoint. Several query parameters (see below) can be applied to filter our results.
Sample request :
```bash Curl theme={null}
curl --location --request GET 'https://api.hub2.io/transfers?from=2023-01-01T00:00:00.000&to=2023-01-01T12:00:00.000&page=1&perPage=100' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getTransfers() {
const response = await fetch('https://api.hub2.io/transfers?from=2023-01-01T00:00:00.000&to=2023-01-01T12:00:00.000&page=1&perPage=100', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const transfers = await response.json();
console.log(transfers);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getTransfers();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/transfers?from=2023-01-01T00:00:00.000&to=2023-01-01T12:00:00.000&page=1&perPage=100',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
Please note the following parameters:
* **Parameter `from` :** identifies the starting date of the transfers retrieval range, the value here is `2023-01-01T00:00:00.000Z`
* **Parameter `to` :** identifies the ending date of the transfers retrieval range, the value here is `2023-01-01T12:00:00.000Z`
* **Parameters `page` and `perPage` :** To control navigation through the result pages.
By default, when no filter is defined, **the 100 last transfers are returned** by this endpoint, ordered by `created_at`, descending.
**For a big number of transfers, paginated requests will be mandatory to retrieve all transactions from the HUB2 API.**
### Pagination
Pagination is available on that endpoint.
The response headers provide the `Content-Range` header to inform of the total number of results, and the header value is in the format `0-99/2453`.
In this particular case, this header indicates that the API returned the first 100 results out of a total of 2453 - so there are 25 pages of 100 results to fetch to retrieve all transfers corresponding to the initial filter.
A good advice is to set date filters in the initial request, so the number of results remains the same while pagination is running (except when `to` is set to a date in the future).
# Introduction
Source: https://docs.hub2.io/integration/en/transfers/transfers_introduction
* [Link to the API reference for transfers](/api-reference/transfers)
* [Link to the OpenAPI documentation](https://api.hub2.io/docs-json)
## Transfer lifecycle
To help getting started with the Transfers API, this illustration shows off how transfers are handled by HUB2 API.
* **IPN : Instant Payment Notification :** Similar to a webhook.
# MSISDN - Blacklist
Source: https://docs.hub2.io/integration/en/transfers/transfers_msisdn_blacklist
The blacklist is implemented at HUB2 level, not at provider level, although providers can integrate similar mechanisms.
Some MSISDN can be blacklisted.
If failed transfers **to a number** are detected, and these failures are recurrent within a short time interval, then the number will be blocked.
As soon as **5 failures are detected over a period of 60 minutes**, the MSISDN will be blacklisted and blocked.
## Unblocking
A MSISDN is automatically unblocked after 7 days.
# Sandbox MSISDN
Source: https://docs.hub2.io/integration/en/transfers/transfers_sandbox_msisdn
In sandbox mode, several MSISDN are available to ease integration:
| MSISDN | Code | |
| :------: | :---------------------- | :------------------------ |
| 00000001 | successful | |
| 00000002 | successful (with delay) | |
| 00000003 | pending (blocked) | |
| 00000100 | failed | `authentication_failed` |
| 00000200 | failed | `unknown_reason` |
| 00000202 | failed | `fraud_suspicion` |
| 00000203 | failed | `forbidden_by_provider` |
| 00000204 | failed | `bad_parameters` |
| 00000206 | failed | `invalid_amount` |
| 00000207 | failed | `canceled` |
| 00000208 | failed | `destination_not_allowed` |
| 00000209 | failed | `unsupported_currency` |
| 00000210 | failed | `fraud_suspicion` |
| 00000301 | failed | `too_many_request` |
| 00000302 | failed | `service_unavailable` |
| 00000303 | failed | `internal_error` |
| 00000305 | failed | `duplicate_request` |
| 00000400 | failed | `invalid_destination` |
Below is the list of MSISDN available per gateway:
| Connector | 00000001 | 00000002 | 00000003 | 00000100 | 00000200 | 00000201 | 00000202 | 00000203 | 00000204 | 00000206 | 00000207 | 00000208 | 00000209 | 00000210 | 00000301 | 00000302 | 00000303 | 00000305 | 00000400 |
| --------------- | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: | :------: |
| mm\_bf\_moov | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| mm\_bj\_moov | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ |
| mm\_bj\_celtiis | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| mm\_\*\_wave | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| mm\_ci\_moov | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ |
| mm\_ci\_mtn | ✔️ | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ |
| mm\_ci\_orange | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ |
| mm\_sn\_free | ✔️ | ❌ | ✔️ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ✔️ |
| mm\_sn\_orange | ✔️ | ✔️ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| mm\_tg\_moov | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ |
\*: within the limits of available countries and providers
# Webhooks behavior
Source: https://docs.hub2.io/integration/en/webhooks/webhooks_behavior
## Number of HTTP call sent
Webhook consumers may receive multiple HTTP calls multiple times for the same event, depending on the event.
Whenever a transaction gets in a final state such as `failed` or `success`, the related webhooks should happen only once.
However for intermediate state such as `pending` or `action_required`, webhooks may be sent more than once. This can happen if the provider data section changed, meaning the HUB2 API got an update from the provider side, but not the kind of update which can help determine the final status of a transaction.
## In case of HTTP failure
In case the target URL is not reachable for any reason (network failure, unavailability of the remote server, ...), the HTTP call will fail.
To overcome this possibility, the HUB2 API has a replay strategy.
### Replay strategy
Whenever a HTTP failure happen, a webhook is replayed after an initial delay of `60` seconds.
Each attempt extends this time exponentially following that piece of code:
```js theme={null}
exponential(delay) {
return function(attemptsMade) {
return Math.round((Math.pow(2, attemptsMade) - 1) * delay);
};
}
```
That will give the following delays for each Nth attempt:
1. Wait 1 minute
2. Wait 3 minutes
3. Wait 7 minutes
4. Wait 15 minutes
5. Wait 31 minutes
6. Wait 63 minutes
7. Wait 127 minutes
8. ...
The replay strategy won't last forever. After **`10`** attempts, the webhook will consider the target URL dead and will stop retrying. This will happen roughly after \~34 hours.
### Timeout
Any target URL of a webhook taking longer than 5 seconds to response will trigger a timeout in HUB2 API and will be considered a failure.
Such a scenario should occur, it will trigger the replay strategy and may be a root cause of receiving multiple times the same webhook.
## Automatic deactivation
This automatic deactivation is only temporary and aims to maintain constant quality of service of the HUB2 API. To avoid any concerns about the quality of the integration, it is best this never happens.
It's possible to activate the webhook again at any time via the available API.
[Webhooks API Documentation](/api-reference/webhooks)
To ensure this deactivation never take place, the remote endpoint receiving the webhook must always return a 2xx HTTP code.
Any other response, or no response, will be considered a failure, and after several tries, will disable the webhook automatically.
# Best practices
Source: https://docs.hub2.io/integration/en/webhooks/webhooks_bestpractices
**These recommendations must be applied before the traffic can go live.**
## HTTP code 2XX
The webhook target URL must **always** respond with a 2XX HTTP code.
*This is to ensure the communication went right. Business logic errors are not to be handled using HTTP error codes.*
## Response times
The target URL must respond **fast**. Expected delay of response is **less than 500ms**.
A grace delay of less than a second may be authorized in exceptional cases.
*This is to maintain an optimal quality of service. Live webhooks with longer delays won't be permitted.*
Please note, those deadlines may be reduced in the future.
Notices will be sent far ahead in case it happens, especially to the slowest integrations to response to the webhook requests.
## Asynchronous handling
In order to achieve these response times, there are simple mechanisms that can be put in place.
Among other things, it's strongly encouraged to process webhooks asynchronously upon reception and immediately response with an HTTP 2xx code.
Example in javascript:
```javascript theme={null}
async function onPaymentIntentCreatedWebhookReceived(req, res) {
await this.verifyHUB2WebhookSignature(req);
this.handlePaymentIntentCreated(req); // async method, event queue ...
res.send('""');
res.status(201).end(); // Send HTTP 2xx right away
}
```
## Split events to multiple webhooks
If a single webhook is registered with all events, its deactivation will prevent all these events to be sent.
A good practice to avoid that behavior is to register a single event per webhook, so any deactivation won't make the other webhooks to stop working too.
# List of available events
Source: https://docs.hub2.io/integration/en/webhooks/webhooks_events_list
Below a complete list of events for which a webhook can be configured:
* `transfer.created`
* `transfer.processing`
* `transfer.succeeded`
* `transfer.failed`
* `transfer_irt.created`
* `transfer_irt.processing`
* `transfer_irt.succeeded`
* `transfer_irt.failed`
* `payment_intent.created`
* `payment_intent.processing`
* `payment_intent.succeeded`
* `payment_intent.action_required`
* `payment_intent.payment_failed`
* `payment.created`
* `payment.pending`
* `payment.succeeded`
* `payment.action_required`
* `payment.failed`
* `provisioning.created`
* `provisioning.succeeded`
* `provisioning.failed`
* `receipt.created`
* `sms.created`
* `sms.sent`
* `sms.received`
* `sms.failed`
* `transaction.payment_collected`
* `transaction.deposit`
* `provider.wave`
# Samples
Source: https://docs.hub2.io/integration/en/webhooks/webhooks_examples
Below are some samples of webhooks the HUB2 API can send.
```json theme={null}
{
"owner": "3",
"type": "transfer.created",
"data": {
"id": "tr_ntP3VFmvRNJ1L5lk5kK3E",
"merchantId": "nS4vWERynqjMDFR20cGmL",
"createdAt": "2024-06-06T10:49:36.122Z",
"updatedAt": "2024-06-06T10:49:36.122Z",
"reference": "ref_01",
"description": "description",
"status": "created",
"amount": 33950,
"currency": "XOF",
"mode": "live",
"destination": {
"type": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"recipientName": "XXX"
},
"fees": [
{
"id": "fee_EhsB9ZYhNuIxZ9JSQaMxN",
"rate": 2,
"type": "percent",
"amount": 679,
"currency": "XOF",
"label": null,
"taxes": []
}
],
"origin": {
"name": "XXX",
"country": "CI"
},
"overrideBusinessName": "XXXX",
"overrideBusinessId": null,
"isIrt": false
},
"test": false,
"id": "evt_q4FFfUVzok5fuZz9rk7ta",
"createdAt": "2024-06-06T10:49:36.212Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "transfer.processing",
"data": {
"id": "tr_dfOVfrBHdyPuD0w4Hgfse",
"merchantId": "K6iaxjfUZg1urpLUbSkXN",
"createdAt": "2024-06-06T10:57:04.304Z",
"updatedAt": "2024-06-06T10:57:04.449Z",
"reference": "ref_01",
"description": "desc_01",
"status": "pending",
"amount": 11000,
"currency": "XOF",
"mode": "live",
"destination": {
"type": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"recipientName": "XXXX"
},
"fees": [
{
"id": "fee_dhjkdTkanhKBt3ckhZl9E",
"rate": 0.8,
"type": "percent",
"amount": 88,
"currency": "XOF",
"label": null,
"taxes": []
}
],
"origin": {
"name": "XXXX",
"country": "CI"
},
"overrideBusinessName": "XXXX",
"overrideBusinessId": null,
"isIrt": false
},
"test": false,
"id": "evt_LCEbahLw6S9JHWUUkgLPE",
"createdAt": "2024-06-06T10:57:04.470Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "transfer.failed",
"data": {
"id": "tr_GvzsTY3HdbGbo1NaPB6Ml",
"merchantId": "nS4vWERynqjMDFR20cGmL",
"createdAt": "2024-06-06T10:49:25.669Z",
"updatedAt": "2024-06-06T10:49:29.535Z",
"reference": "4lDfuTZ9",
"description": "desc_001",
"status": "failed",
"amount": 11900,
"currency": "XOF",
"mode": "live",
"destination": {
"type": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"recipientName": "XXXX"
},
"fees": [
{
"id": "fee_1Lsk4j0l1pH0KQ4D2ESLn",
"rate": 2,
"type": "percent",
"amount": 238,
"currency": "XOF",
"label": null,
"taxes": []
}
],
"failureCause": {
"code": "internal_error",
"message": "Le transfert a échoué, une erreur interne est survenue. Notre service technique est au courant et travaille à sa résolution."
},
"origin": {
"name": "XXXX",
"country": "CI"
},
"overrideBusinessName": "XXXX",
"overrideBusinessId": null,
"isIrt": false
},
"test": false,
"id": "evt_JhCfP5hZX60E7CN8wTbbW",
"createdAt": "2024-06-06T10:49:29.553Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "transfer.succeeded",
"data": {
"id": "tr_rneqc0ufyVLUuQZrgXAIq",
"merchantId": "dxpIyVe2AfMDoWwr5XyPM",
"createdAt": "2024-06-06T09:48:39.457Z",
"updatedAt": "2024-06-06T09:48:40.119Z",
"reference": "ref_0021",
"description": "Unit Testing - 30 Test transfer",
"status": "successful",
"amount": 1000,
"currency": "XOF",
"mode": "live",
"destination": {
"type": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"recipientName": "..."
},
"fees": [
{
"id": "fee_DAuH52rhjNfPHfnRcieCJ",
"rate": 1.6,
"type": "percent",
"amount": 16,
"currency": "XOF",
"label": null,
"taxes": []
}
],
"origin": {
"name": "XXX",
"country": "CI"
},
"overrideBusinessName": " XXX",
"overrideBusinessId": null,
"isIrt": false
},
"test": false,
"id": "evt_grPErVipwaRVWaXh1mx37",
"createdAt": "2024-06-06T09:48:40.242Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment_intent.created",
"data": {
"id": "pi_Vjdcw4aV8LPn9e0sD92gr",
"createdAt": "2024-06-06T10:13:04.414Z",
"updatedAt": "2024-06-06T10:13:04.415Z",
"merchantId": "1092",
"purchaseReference": "p_ref_01",
"customerReference": "c_ref_01",
"amount": 5000,
"currency": "XOF",
"token": "XXX",
"status": "payment_required",
"overrideBusinessName": null,
"payments": [],
"mode": "live"
},
"test": false,
"id": "evt_jYRsQUGrFugLxmmNevya9",
"createdAt": "2024-06-06T10:13:04.448Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment_intent.processing",
"data": {
"id": "pi_8UG3Hbof1n1XitPJm1nUG",
"createdAt": "2024-06-06T10:17:40.305Z",
"updatedAt": "2024-06-06T10:17:42.933Z",
"merchantId": "1013",
"purchaseReference": "p_ref_01",
"customerReference": "c_ref_01",
"amount": 9455,
"currency": "XOF",
"token": "XXX",
"status": "processing",
"overrideBusinessName": null,
"payments": [
{
"id": "pay_Yksq7VTUMQiDu5yR3LjAP",
"intentId": "pi_8UG3Hbof1n1XitPJm1nUG",
"createdAt": "2024-06-06T10:17:42.855Z",
"updatedAt": "2024-06-06T10:17:42.933Z",
"amount": 9455,
"currency": "XOF",
"status": "pending",
"method": "mobile_money",
"country": "CI",
"provider": "wave",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_ZIp4Nn4CiclTRdJ8vXjCo",
"label": "payments.payment_processor_fee",
"rate": 2.5,
"rateType": "percent",
"amount": 237,
"taxes": [
{
"id": "tax_TvP2DXWqjcjyEhMeC1UON",
"feeId": "fee_ZIp4Nn4CiclTRdJ8vXjCo",
"taxType": "tva",
"type": "percent",
"value": "42.66"
}
]
}
]
}
],
"mode": "live"
},
"test": false,
"id": "evt_Xj0QP8b5NqYgXq8rg3IgB",
"createdAt": "2024-06-06T10:17:42.968Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment_intent.action_required",
"data": {
"id": "pay_Wwpm3BarAzK1nFsaChILY",
"createdAt": "2024-06-06T09:48:40.202Z",
"updatedAt": "2024-06-06T09:48:41.018Z",
"merchantId": "3",
"purchaseReference": "ref_001",
"customerReference": "cref_111",
"amount": 4950,
"currency": "XOF",
"token": "XXX",
"status": "action_required",
"overrideBusinessName": null,
"payments": [
{
"id": "pay_Wwpm3BarAzK1nFsaChILY",
"intentId": "pi_rRiK6_4YehnDKZtlv6ce5",
"createdAt": "2024-06-06T09:48:40.419Z",
"updatedAt": "2024-06-06T09:48:41.018Z",
"amount": 4950,
"currency": "XOF",
"status": "pending",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_XTyrlgrls3nprRNXjTIFC",
"label": "payments.payment_processor_fee",
"rate": 1.5,
"rateType": "percent",
"amount": 75,
"taxes": [
{
"id": "tax_14NEcKn3mbIWRIJgSboWD",
"feeId": "fee_XTyrlgrls3nprRNXjTIFC",
"taxType": "tva",
"type": "percent",
"value": "13.5"
}
]
}
],
"nextAction": {
"type": "ussd",
"message": "Vous allez bientôt recevoir un message USSD sur votre téléphone pour valider la transaction."
}
}
],
"mode": "live",
"nextAction": {
"type": "ussd",
"message": "Vous allez bientôt recevoir un message USSD sur votre téléphone pour valider la transaction."
}
},
"test": false,
"id": "evt_NeQIBbHU415HtytWfpdI9",
"createdAt": "2024-06-06T09:48:41.049Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment_intent.payment_failed",
"data": {
"id": "pi_ARPsD0D1Ip6h6t7imj_td",
"createdAt": "2024-06-06T09:36:15.932Z",
"updatedAt": "2024-06-06T10:15:06.637Z",
"merchantId": "3",
"purchaseReference": "p_ref_01",
"customerReference": "c_ref_01",
"amount": 300,
"currency": "XOF",
"token": "XXX",
"status": "payment_required",
"overrideBusinessName": "XXX",
"payments": [
{
"id": "pay_Ibrv2miHcxilNXuCCVWJH",
"intentId": "pi_ARPsD0D1Ip6h6t7imj_td",
"createdAt": "2024-06-06T09:36:16.155Z",
"updatedAt": "2024-06-06T10:15:06.637Z",
"amount": 300,
"currency": "XOF",
"status": "failed",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "0704903330",
"fees": [
{
"currency": "XOF",
"id": "fee_UxpPlDEEGRpzGUNfr2mjv",
"label": "payments.payment_processor_fee",
"rate": 1.6,
"rateType": "percent",
"amount": 5,
"taxes": [
{
"id": "tax_cMk0WxrvVumkzvvjuihZv",
"feeId": "fee_UxpPlDEEGRpzGUNfr2mjv",
"taxType": "tva",
"type": "percent",
"value": "0.9"
}
]
}
],
"failure": {
"code": "forbidden_by_provider",
"message": "Le paiement a été refusé par l'opérateur."
}
}
],
"lastPaymentFailure": {
"code": "forbidden_by_provider",
"message": "Le paiement a été refusé par l'opérateur."
},
"mode": "live"
},
"test": false,
"id": "evt_a1qAnpgqMXwWuEl3Y2CWM",
"createdAt": "2024-06-06T10:15:06.676Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment.pending",
"data": {
"id": "pay_Yw1lPIiq4NXS0GhDghSCf",
"intentId": "pi_OIULZrtgDsFyW740lSte4",
"createdAt": "2024-06-06T10:47:54.087Z",
"updatedAt": "2024-06-06T10:47:54.146Z",
"amount": 540,
"currency": "XOF",
"status": "pending",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_gHuiX6MPbxzelvNiYFaX5",
"label": "payments.payment_processor_fee",
"rate": 1.6,
"rateType": "percent",
"amount": 9,
"taxes": [
{
"id": "tax_5AMRKK4mvhJAsU0brDuPF",
"feeId": "fee_gHuiX6MPbxzelvNiYFaX5",
"taxType": "tva",
"type": "percent",
"value": "1.62"
}
]
},
{
"currency": "XOF",
"id": "fee_AltZakt33ORsJOWoWddVb",
"label": "payments.modulo_5_roundup_fee",
"rate": 3,
"rateType": "flat",
"amount": 3,
"taxes": [
{
"id": "tax_RTxQuFscObmPtAAdVjCrb",
"feeId": "fee_AltZakt33ORsJOWoWddVb",
"taxType": "tva",
"type": "percent",
"value": "0.54"
}
]
}
]
},
"test": false,
"id": "evt_55TvVdQwnOtc2PKpSYGob",
"createdAt": "2024-06-06T10:47:54.184Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment.action_required",
"data": {
"id": "pay_NXlR88JW89yXQSGP4oDLb",
"intentId": "pi_coTk5ZhyvqVFBVUul_OZO",
"createdAt": "2024-06-06T10:44:04.636Z",
"updatedAt": "2024-06-06T10:44:05.842Z",
"amount": 18450,
"currency": "XOF",
"status": "pending",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_AqKZpdHaSiI5jOTHuLn4G",
"label": "payments.payment_processor_fee",
"rate": 2,
"rateType": "percent",
"amount": 369,
"taxes": [
{
"id": "tax_RR4njYvTrV4gu9VzX9hM0",
"feeId": "fee_AqKZpdHaSiI5jOTHuLn4G",
"taxType": "tva",
"type": "percent",
"value": "66.42"
}
]
}
],
"nextAction": {
"type": "ussd",
"message": "Vous allez bientôt recevoir un message USSD sur votre téléphone pour valider la transaction."
}
},
"test": false,
"id": "evt_hpySeIXRQCUzXqX8B9c7K",
"createdAt": "2024-06-06T10:44:05.888Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment.failed",
"data": {
"id": "pay_RlL8voNJYNxVGC8wmj50f",
"intentId": "pi_N4i4MrVJM32heF5KLlqsf",
"createdAt": "2024-06-06T10:45:14.299Z",
"updatedAt": "2024-06-06T10:46:33.353Z",
"amount": 23065,
"currency": "XOF",
"status": "failed",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_rR4XkFLUKhL8MYk9lsGFd",
"label": "payments.payment_processor_fee",
"rate": 2,
"rateType": "percent",
"amount": 462,
"taxes": [
{
"id": "tax_iUzzGD3heWrpILYqBNxTI",
"feeId": "fee_rR4XkFLUKhL8MYk9lsGFd",
"taxType": "tva",
"type": "percent",
"value": "83.16"
}
]
}
],
"failure": {
"code": "authentication_failed",
"message": "La validation du paiement par le client a échoué, le paiement a été annulé."
}
},
"test": false,
"id": "evt_XLFQHgszsCpvIyPyhCSlv",
"createdAt": "2024-06-06T10:46:33.413Z"
}
```
```json theme={null}
{
"owner": "3",
"type": "payment.succeeded",
"data": {
"id": "pay_KzParl9RAAzhaWVCIf3FO",
"intentId": "pi_RR1FVRA6sP46K7ryRHzyk",
"createdAt": "2024-06-06T09:48:26.004Z",
"updatedAt": "2024-06-06T09:48:40.910Z",
"amount": 2000,
"currency": "XOF",
"status": "successful",
"method": "mobile_money",
"country": "CI",
"provider": "orange",
"number": "00000001",
"fees": [
{
"currency": "XOF",
"id": "fee_kC9TDTKyfzaHdJW9BUN09",
"label": "payments.payment_processor_fee",
"rate": 2.7,
"rateType": "percent",
"amount": 54,
"taxes": [
{
"id": "tax_yrcpDQ3RLMx1usf2gPISb",
"feeId": "fee_kC9TDTKyfzaHdJW9BUN09",
"taxType": "tva",
"type": "percent",
"value": "10.395"
}
]
}
]
},
"test": false,
"id": "evt_HdpvK6G9lh3m9Z7oLEYv1",
"createdAt": "2024-06-06T09:48:40.955Z"
}
```
# Integration
Source: https://docs.hub2.io/integration/en/webhooks/webhooks_integration
Here are the steps to follow for a proper integration of webhooks :
This endpoint must be able to process a POST request as below:
* **URL**: `https://website.com/webhook_payment`
* **METHOD**: `POST`
* **HEADERS**:
```
ContentType: application/json
HUB2-Signature: s1=XXXXXX,s0=XXXXXX
```
* **BODY**:
```json theme={null}
{
"type": "payment_intent.created",
"data": {
"id":"pi_9lEjld8yF2USvijl7mnId",
"merchantId":"10",
"createdAt":"2021-03-03T11:16:39.280Z",
"updatedAt":"2021-03-03T11:16:39.288Z",
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudElkIjoiMTAiLCJtb2RlIjoic2FuZGJveCIsInBheW1lbnRJZCI6InBpXzlsRWpsZDh5RjJVU3Zpamw3bW5JZCIsImlhdCI6MTYxNDc3MDE5OX0.fkak8HAfHvcCeCxtzBEx1bE39oWl4vkGNgWdhnAmhXo",
"purchaseReference":"ref_2020_11_10_001",
"customerReference":"cust_01924059",
"status":"payment_required",
"amount":100,
"currency":"XOF",
"payments":[],
"mode":"sandbox"
},
"id": "evt_O80mmIF5CNrBMbZIyHJye",
"createdAt": "2021-03-03 11:16:39.685+00"
}
```
This is an example of webhook issued by a `payment_intent.created` event.
The HUB2 API does have a dedicated endpoint for this action. It must be called with the following body:
```json theme={null}
{
"url": "https://website.com/webhook_payment",
"events": [
"payment.created",
"payment_intent.created",
],
"description": "This is a webhook trigger upon payment & payment_intent creation",
"metadata": {}
}
```
Here are samples of code to perform this:
```bash Curl theme={null}
curl -X POST \
-H "Content-Type: application/json" \
-H "ApiKey: " \
-H "MerchantId: " \
-H "Environment: " \
-d '{ "url": "https://my.webhook.target", "events": ["payment.created", "payment_intent.created"], "description": "This is a webhook trigger upon payment & payment_intent creation", "metadata": {} }' \
https://api.hub2.io/webhooks
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function createWebhook() {
const response = await fetch('https://api.hub2.io/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ApiKey': 'Your API KEY',
'MerchantId': 'Your merchant Id',
'Environment': 'Your API KEY environment \'live\' or \'sandbox\''
},
body: JSON.stringify({
"url": "https://my.webhook.target",
"events": ["payment.created", "payment_intent.created"],
"description": "This is a webhook trigger upon payment & payment_intent creation",
"metadata": {}
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
createWebhook();
```
```python Python theme={null}
import requests
import json
url = 'https://api.hub2.io/webhooks'
headers = {
'Content-Type': 'application/json',
'ApiKey': 'Your API KEY',
'MerchantId': 'Your merchant Id',
'Environment': 'Your API KEY environment \'live\' or \'sandbox\'',
}
data = {
"url": "https://my.webhook.target",
"events": ["payment.created", "payment_intent.created"],
"description": "This is a webhook trigger upon payment & payment_intent creation",
"metadata": {}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
This is a sample of the HUB2 API response to this call:
```json theme={null}
{
"id": "wh_z1urYtVFgEebtcj8fxp4v",
"createdAt": "2020-10-15T12:09:49.355Z",
"updatedAt": "2020-10-15T12:09:49.355Z",
"mode": "live",
"description": "This is a webhook trigger upon payment & payment_intent creation",
"events": ["payment.created", "payment_intent.created"],
"metadata": { },
"secret": "5257a869e7ecebeda32affa62cd...",
"status": "enabled",
"url": "https://website.com/webhook_payment"
}
```
This response contains a secret associated with the created webhook: `"secret": "5257a869e7ecebeda32affa62cd..."`.
The secret must be kept carefully. It will be mandatory to verify the integrity of the webhook.
For more details on the webhooks API, check the following documentation: [Webhooks API Documentation](/api-reference/webhooks)
**This step is mandatory to ensure that no one can forge fake webhooks.**
Skipping this step may offer a vulnerability to harmful people, allowing them to automatically validate transactions themselves.
To ensure the webhook received comes from the HUB2 API, it is imperative to check the signature of the body (payload).
### a. Get the secret from the webhook registration.
This secret was generated in step 2, `"secret": "5257a869e7ecebeda32affa62cd..."` in our example.
### b. Sign the payload.
Extract the contents of the BODY from the POST request sent by HUB2 first (see [Json response](#1-cr%C3%A9er-a-callback-endpoint-for-webhooks-in-your-server-environment)). It should result in a characters string that must be signed via HMAC256 and the `secret`.
Here are some examples of procedures depending on the programming language used :
```js Javascript theme={null}
// Example with Javascript/NestJS
import { createHmac, Hmac } from 'crypto';
sign(json: string, secret: string): string {
const hmac: Hmac = createHmac('sha256', secret);
hmac.update(json);
return hmac.digest('hex');
}
```
```python Python theme={null}
# Example with Python
import hashlib
import hmac
def sign(json, secret):
hmac_obj = hmac.new(secret.encode('utf-8'), json.encode('utf-8'), hashlib.sha256)
return hmac_obj.hexdigest()
```
```java Java theme={null}
// Example with java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public String sign(String json, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] hmacBytes = mac.doFinal(json.getBytes());
return javax.xml.bind.DatatypeConverter.printHexBinary(hmacBytes).toLowerCase();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
// Handle exceptions
}
}
```
```ruby Ruby theme={null}
# Example with ruby
require 'openssl'
def sign(json, secret)
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), secret, json)
end
```
```c# C# theme={null}
// Example with C#
using System.Security.Cryptography;
using System.Text;
public string Sign(string json, string secret)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret))
{
byte[] bytes = Encoding.UTF8.GetBytes(json);
byte[] hashBytes = hmac.ComputeHash(bytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
}
}
```
```php PHP theme={null}
// Example with PHP
function sign($json, $secret) {
$hash = hash_hmac('sha256', $json, $secret);
return $hash;
}
```
The request body must be in JSON string. Either the framework used directly returns a JSON string or it must be transformed to JSON format with a call like `JSON.stringify()` on the body of the request.
### c. Check signature.
Finally, the computed signature must be compared to the one provided by HUB2, located in the HEADERS of the POST request.
```
HUB2-Signature: s1=ABCD,s0=XYZ
```
* `HUB2-Signature`: Header name
* `s1`: Signature computed using current `secret`.
* `s0`: Signature computed using old secret `oldSecret`if present.
When updating a `secret`, the previous active `secret` becomes `oldSecret` for a period of 24 hours and is used to generate the `s0` signature.
# Introduction
Source: https://docs.hub2.io/integration/en/webhooks/webhooks_overview
A webhook is a callback function based on the HTTP protocol.
It allows a remote API to establish event-oriented communication with the HUB2 API.
In simple words, events can be subscribed on the HUB2 API and whenever such event occurs, the remote API will be called using the HTTP protocol.
***
To subscribe a webhook, the list of events to listen to must be provided alongside an URL which will be called.
Doing so remove the necessity to perform polling on the HUB2 API to know the current status of a transaction. Instead the HUB2 API will automatically reach the provided URL with all useful data in the request body to let the remote API know an update occurred.
The advantages of webhooks
* They replace continuous interrogation. It's no longer needed to call HUB2 API at regular intervals (polling), saving valuable resources for both sides.
* Webhooks are quick to configure.
* Instant notification whenever an event of interest occurs, preventing possible delays from the polling method.
# Check Merchant Balance
Source: https://docs.hub2.io/api-reference/balance/check-merchant-balance
get /balance
Returns a summary of a merchant accounts balances (collection, transfer and transfer irt accounts) .
# Create a kyb
Source: https://docs.hub2.io/api-reference/compliance/create-a-kyb
post /compliance/kyb
# Create a kyb transaction
Source: https://docs.hub2.io/api-reference/compliance/create-a-kyb-transaction
post /compliance/transaction
# Delete a kyb
Source: https://docs.hub2.io/api-reference/compliance/delete-a-kyb
delete /compliance/kyb/{name}
# Delete a kyb transaction
Source: https://docs.hub2.io/api-reference/compliance/delete-a-kyb-transaction
delete /compliance/transaction/{reference}
# Fetch a kyb list
Source: https://docs.hub2.io/api-reference/compliance/fetch-a-kyb-list
get /compliance/kyb
# Fetch a kyb transactions list
Source: https://docs.hub2.io/api-reference/compliance/fetch-a-kyb-transactions-list
get /compliance/transaction
# Get kyc information
Source: https://docs.hub2.io/api-reference/compliance/get-kyc-information
get /compliance/kyc
# Retrieve a kyb by name
Source: https://docs.hub2.io/api-reference/compliance/retrieve-a-kyb-by-name
get /compliance/kyb/{name}
# Retrieve a kyb transaction by reference
Source: https://docs.hub2.io/api-reference/compliance/retrieve-a-kyb-transaction-by-reference
get /compliance/transaction/{reference}
# Update a kyb
Source: https://docs.hub2.io/api-reference/compliance/update-a-kyb
put /compliance/kyb/{name}
# Get countries
Source: https://docs.hub2.io/api-reference/data/get-countries
get /data/countries
Returns the list of countries where Hub2 is available
# Get providers
Source: https://docs.hub2.io/api-reference/data/get-providers
get /data/providers
Returns the list of providers available with Hub2
# Create IRT transfer
Source: https://docs.hub2.io/api-reference/irt/create-irt-transfer
post /irt/transfer
Make an IRT transfer
# Retrieve a IRT transfers collection
Source: https://docs.hub2.io/api-reference/irt/retrieve-a-irt-transfers-collection
get /irt/transfer
Return a list of IRT transfers, eventually filtered by criteria.
# Execute payment
Source: https://docs.hub2.io/api-reference/payment-on-terminal/execute-payment
post /terminal/payments
# Retrieve a payment
Source: https://docs.hub2.io/api-reference/payment-on-terminal/retrieve-a-payment
get /terminal/payments/{id}
# Close payment links
Source: https://docs.hub2.io/api-reference/payment_links/close_payment_links
patch /payment-links/{id}/close
# Create Payment Links
Source: https://docs.hub2.io/api-reference/payment_links/create_payment_links
POST https://api.hub2.io/payment-links
This endpoint allows you to generate a secure payment link for receiving payments from your customers.
The Payment Links API allows you to accept payments from customers using a payment link URL. The payment link URL takes your shopper to an HUB2 hosted secure checkout page where they can make the payment with their preferred payment method.
## Request
Reference of the Purchase on your system (e.g., `EM_TEST_1006`)
The additional description text that appears under the title in the payment checkout page. (e.g., `Text displayed for the description`)
The payment amount (e.g., `500`).
Minimum amount for payment links is 500 XOF.
The payment currency. (e.g., `XOF`)
Country where the customers should pay (e.g., `CI`)
Specifies whether the payment link can be used once or multiple times. Default value is "single\_use" for the current version of the API. (e.g., `CI`)
The number of the customer associated with the purchaseReference. (e.g., `0707070707`)
Expiration date of the payment links (ISO 8601 format). Default value is 15mins (e.g., `2025-11-01T00:00:00.000Z`)
## Response
The unique Payment Links ID
The unique Payment Links URL
The current status of the Payment Links. Possible values: `active`, `completed`, `closed`, `expired`
Reference of the Purchase associated with the payment links
The payment amount
The currency of the payment (e.g., `XOF`, `XAF`)
Maximum payments allowed for a payment links.
Maximum payments attempts allowed for a payment links.
The date and time when the payment links was created
The date and time when the payment links was last updated
The date and time when the payment links will expire
The list of payment methods allowed for the payment link created
The URL to which the user is redirected upon a successful payment
The URL to which the user is redirected if the payment fails
The customer PhoneNumber set for the payment.
```bash Payment Links Request theme={null}
curl --location 'https://api.hub2.io/payment-links' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: live' \
--header 'Content-Type: application/json' \
--data '{
"purchaseReference": "EM_TEST_0016",
"description":"Payment Links are the fastest way to start accepting payments from your customers",
"amount": 100,
"currency": "XOF",
"type":"single_use",
"expirationDate": "2025-10-27T18:30:00",
"paymentMethods": [
"mobile_money"
],
"country": "CI",
"providers": [
"Orange",
"Moov",
"MTN"
]
}'
```
```json Response theme={null}
{
"id": "pl_PX0YZsb3iFg23vX7hWbcC",
"url": "https://pay.preprod.hub2.io/pl_PX0YZsb3iFg23vX7hWbcC",
"status": "active",
"purchaseReference": "EM_TEST_0016",
"amount": 100,
"currency": "XOF",
"maximumPayments": 1,
"maximumAttempts": 3,
"createdAt": "2025-10-27T09:17:52.927Z",
"updatedAt": "2025-10-27T09:17:52.927Z",
"expirationDate": "2025-10-27T18:30:00.000Z",
"operators": {
"mobile_money": [
"Orange",
"MTN",
"Moov"
]
},
"merchantId": "16",
"description": "Payment Links are the fastest way to start accepting payments from your customers. Simply input your currency and the amount you would like to charge your customer and a link will be generated. Links can be personalised.271025-09.13",
"successUrl": "https://hub2.io",
"failureUrl": "https://hub2.io",
"customerPhoneNumber": null,
"type": "single_use"
}
```
# List Payment Links
Source: https://docs.hub2.io/api-reference/payment_links/list_payment_links
get /payment-links
Retrieve a list of payment links with optional filtering
# Get Payment Link with Attempts
Source: https://docs.hub2.io/api-reference/payment_links/list_payment_links_with_attempts
get /payment-links/{id}
Retrieve detailed information about a payment link including payment attempts
# Attempt a payment on a PaymentIntent object
Source: https://docs.hub2.io/api-reference/payments/attempt-a-payment-on-a-paymentintent-object
post /payment-intents/{id}/payments
This is the endpoint to use whenever you want to attempt a payment on a payment intent.
It will create a payment object associated with the intent which will contains all the informations about the payment.
Whenever a payment fails or don't have the appropriate amount to match the intent, other payments can be created on the same intent.
# Authenticate the current payment
Source: https://docs.hub2.io/api-reference/payments/authenticate-the-current-payment
post /payment-intents/{id}/authentication
Some circuit have authentication restrictions which requires an extra step.
This endpoint is here to allow you to make that extra steps. It happens after a payment has been requested and when that payment moved to the "action_required" state.
# Create a PaymentIntent object
Source: https://docs.hub2.io/api-reference/payments/create-a-paymentintent-object
post /payment-intents
This endpoint is the first entry point of the PAY-IN circuit.
First of all, you must create a payment intent which describes your intention to collect an amount of money in a currency.
Later on, you will perform payments associated with this payment intent.
Please do save the fields returned by this endpoint, especially the field 'id' and 'token'.
That will help you identity the payment intent in our platform whenever you want to retrieve it.
# Retrieve a PaymentIntent collection
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-paymentintent-collection
get /payment-intents
This endpoints is here to provide you an efficient way of retrieving multiple payment intents at the same time.
Everything is provided in the request's headers to perform a proper pagination on the results.
# Retrieve a PaymentIntent object
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-paymentintent-object
get /payment-intents/{id}
This endpoint allows you to fetch an existing payment intent from our database.
You must either provide the "id" or the "token" for us to find that payment intent.
Retrieving using the "token" is the fastest way and uses a more permissive rate limit (10 requests per 5 seconds).
# Retrieve a PaymentIntent status
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-paymentintent-status
get /payment-intents/{id}/status
This endpoint allows you to fetch the status of an existing payment intent from our database.
You must either provide the "id" or the "token" for us to find that payment intent.
Retrieving using the "token" is the fastest way and uses a more permissive rate limit (10 requests per 5 seconds).
# Retrieve a Payments collection
Source: https://docs.hub2.io/api-reference/payments/retrieve-a-payments-collection
get /payments
Return a list of Payments, eventually filtered by criteria.
# Retrieve configured URLs
Source: https://docs.hub2.io/api-reference/payments/retrieve-configured-urls
get /payment-intents/url/{token}
This endpoint fetch the merchant's preference and return the redirection URL saved in there.
This is mostly used for the Hub2 modal feature.
You must provide the payment intent token for that.
# Retrieve fees
Source: https://docs.hub2.io/api-reference/payments/retrieve-fees
get /payment-intents/{id}/payment-fees
This endpoint allows you to retrieve the fees associated with a payment intent.
Fees are applied per payment and this endpoints will group all the fees in an array.
# Retrieve the status payment object
Source: https://docs.hub2.io/api-reference/payments/retrieve-the-status-payment-object
get /payments/{id}/status
# Synchronously attempt a payment on a payment intent
Source: https://docs.hub2.io/api-reference/payments/synchronously-attempt-a-payment-on-a-payment-intent
post /payment-intents/{id}/payments/sync
This is the same endpoint but the payment is handled synchronously, it means it will wait for the provider response before returning the status of the payment intent. This endpoint is not the favored way to integrate our api and is not available for all merchants and for all providers.
# Create a Provisioning
Source: https://docs.hub2.io/api-reference/provisioning/create-a-provisioning
post /provisionings
Move funds from merchant's collection account to transfer account
# Retrieve a Provisioning from given id
Source: https://docs.hub2.io/api-reference/provisioning/retrieve-a-provisioning-from-given-id
get /provisionings/{id}
# Retrieve a Provisionings collection
Source: https://docs.hub2.io/api-reference/provisioning/retrieve-a-provisionings-collection
get /provisionings
# Retrieve a Receipt
Source: https://docs.hub2.io/api-reference/receipt/retrieve-a-receipt
get /receipts/{id}
Retrieve a sucessful or failed transfer or payment receipt
# Retrieve a Receipt as an HTML representation
Source: https://docs.hub2.io/api-reference/receipt/retrieve-a-receipt-as-an-html-representation
get /receipts/html/{id}
Retrieve a sucessful or failed transfer or payment receipt as HTML
# Create a new recipient
Source: https://docs.hub2.io/api-reference/recipients/create-recipient
post /recipient
# Create a refund
Source: https://docs.hub2.io/api-reference/refunds/create-refund
POST https://api.hub2.io/refunds
This endpoint allows you to create a refund for a successful transfer.
Creates a refund for a previously successful transfer. The refund will be processed asynchronously and you will receive webhook notifications about status changes.
## Request
The transfer ID to refund (e.g., `tr_000000000000000000011`)
Zendesk ticket ID associated with the refund (e.g., `zd_123456`)
## Response
The unique refund ID
The current status of the refund. Possible values: `created`, `pending`, `pending_manual`, `successful`, `failed`
The original transfer ID that is being refunded
The refund amount (same as original transfer)
The currency of the refund (e.g., `XOF`, `USD`)
The reason for the refund
The payment method used for the refund. Possible values: `mobile_money`, `bank_transfer`
Whether fallback methods are allowed for this refund
Whether a fallback method was used for this refund
Additional metadata associated with the refund
The Zendesk ticket ID associated with the refund
The date and time when the refund was created
The date and time when the refund was last updated
```bash Example Request theme={null}
curl --location --request POST 'https://api.hub2.io/refunds' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
}'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function createRefund() {
const response = await fetch('https://api.hub2.io/refunds', {
method: 'POST',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
})
});
if (response.ok) {
const result = await response.json();
console.log(result);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
createRefund();
```
```python Python theme={null}
import json
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
data = {
"id": "tr_000000000000000000011",
"zendeskTicketId": "zd_123456"
}
response = requests.post(
'https://api.hub2.io/refunds',
headers=headers,
data=json.dumps(data)
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
```json Response theme={null}
{
"id": 123,
"status": "created",
"transactionId": "tr_000000000000000000011",
"amount": 2000,
"currency": "XOF",
"reason": "Customer requested refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_123456",
"createdAt": "2023-01-01T12:00:00.000Z",
"updatedAt": "2023-01-01T12:00:00.000Z"
}
```
# Retrieve a refund
Source: https://docs.hub2.io/api-reference/refunds/retrieve-a-refund
GET https://api.hub2.io/refunds/{id}
This endpoint allows you to retrieve the details of a specific refund.
Retrieves the details of a refund using its unique ID.
## Path Parameters
The unique refund ID
## Response
The unique refund ID
The current status of the refund. Possible values: `created`, `pending`, `pending_manual`, `successful`, `failed`
The original transfer ID that is being refunded
The refund amount (same as original transfer)
The currency of the refund (e.g., `XOF`, `USD`)
The reason for the refund
The payment method used for the refund. Possible values: `mobile_money`, `bank_transfer`
Whether fallback methods are allowed for this refund
Whether a fallback method was used for this refund
Additional metadata associated with the refund
The Zendesk ticket ID associated with the refund
The date and time when the refund was created
The date and time when the refund was last updated
```bash Example Request theme={null}
curl --location --request GET 'https://api.hub2.io/refunds/123' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getRefund() {
const response = await fetch('https://api.hub2.io/refunds/123', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const refund = await response.json();
console.log(refund);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getRefund();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/refunds/123',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
```json Response theme={null}
{
"id": 123,
"status": "successful",
"transactionId": "tr_000000000000000000011",
"amount": 2000,
"currency": "XOF",
"reason": "Customer requested refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_123456",
"createdAt": "2023-01-01T12:00:00.000Z",
"updatedAt": "2023-01-01T12:30:00.000Z"
}
```
# Retrieve refunds collection
Source: https://docs.hub2.io/api-reference/refunds/retrieve-refunds-collection
GET https://api.hub2.io/refunds
This endpoint allows you to retrieve a list of refunds with filtering and pagination options.
Retrieves a paginated list of refunds. You can filter the results using various query parameters.
## Query Parameters
Filter by refund ID
Filter refunds created after this date (ISO 8601 format, e.g., `2023-01-01T00:00:00.000Z`)
Filter refunds created before this date (ISO 8601 format, e.g., `2023-01-01T12:00:00.000Z`)
Filter by refund status. Possible values: `created`, `pending`, `pending_manual`, `successful`, `failed`
Filter by transfer reference (original transfer reference)
Page number for pagination
Number of results per page (max 100)
## Response
Array of refund objects
The unique refund ID
The current status of the refund
The original transfer ID that is being refunded
The refund amount
The currency of the refund
The reason for the refund
The payment method used for the refund
Whether fallback methods are allowed
Whether a fallback method was used
Additional metadata associated with the refund
The Zendesk ticket ID associated with the refund
The date and time when the refund was created
The date and time when the refund was last updated
Pagination information
Current page number
Number of results per page
Total number of refunds
Total number of pages
```bash Example Request theme={null}
curl --location --request GET 'https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: sandbox' \
--header 'Content-Type: application/json'
```
```typescript Typescript theme={null}
import fetch from 'node-fetch';
async function getRefunds() {
const response = await fetch('https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100', {
method: 'GET',
headers: {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json'
}
});
if (response.ok) {
const refunds = await response.json();
console.log(refunds);
} else {
console.log(`HTTP Status: ${response.status}`);
}
}
getRefunds();
```
```python Python theme={null}
import requests
headers = {
'ApiKey': '[REDACTED]',
'MerchantId': '[REDACTED]',
'Environment': 'sandbox',
'Content-Type': 'application/json',
}
response = requests.get(
'https://api.hub2.io/refunds?fromDate=2023-01-01T00:00:00.000&toDate=2023-01-01T12:00:00.000&page=1&perPage=100',
headers=headers
)
if response.status_code == 200:
print(response.json())
else:
print(f'HTTP Status: {response.status_code}')
```
```json Response theme={null}
{
"data": [
{
"id": 123,
"status": "successful",
"transactionId": "tr_000000000000000000011",
"amount": 2000,
"currency": "XOF",
"reason": "Customer requested refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_123456",
"createdAt": "2023-01-01T12:00:00.000Z",
"updatedAt": "2023-01-01T12:30:00.000Z"
},
{
"id": 124,
"status": "pending",
"transactionId": "tr_000000000000000000012",
"amount": 1500,
"currency": "XOF",
"reason": "Merchant initiated refund",
"paymentMethod": "mobile_money",
"fallbackAllowed": true,
"fallbackUsed": false,
"metadata": {},
"zendeskTicketId": "zd_789012",
"createdAt": "2023-01-01T11:30:00.000Z",
"updatedAt": "2023-01-01T11:30:00.000Z"
}
],
"pagination": {
"page": 1,
"perPage": 100,
"total": 2,
"totalPages": 1
}
}
```
# Get a list of sms sent
Source: https://docs.hub2.io/api-reference/sms/get-a-list-of-sms-sent
get /sms/list
Retrieve a list of sms sent between two date.
# Retrieve number of sms sent
Source: https://docs.hub2.io/api-reference/sms/retrieve-number-of-sms-sent
get /sms/count
Retrieve number of sms sent between two date
# Send Sms
Source: https://docs.hub2.io/api-reference/sms/send-sms
post /sms
Send Sms to different destinations.
# Create a submerchant
Source: https://docs.hub2.io/api-reference/submerchants/create_submerchants
POST https://api.hub2.io/submerchants
This endpoint allows you to onboard a new submerchant and generate their unique ID for payment processing and transfers.
The Submerchant API allows you to create submerchant so that you can use their unique ID for transactions while ensuring compliance with regulatory requirements.
## Request Parameters
Name of the submerchant (e.g., `Submerchant Name`)
A description of the submerchant (e.g., `Submerchant description`).
This field can be used to give information regarding submerchant activity.
The street address of the submerchant (e.g., `RIVIERA BEVERLY HILLSt`).
The postal code or zip code of the submerchant (e.g., `00225`).
The city of the submerchant (e.g., `ABIDJAN`).
The phone number of the submerchant (e.g., `+2250707070707`).
## Response
**HTTP Status: 201 Created**
The unique ID for the submerchant (e.g., `wwIxF40ciS6NVQddoa5BQ`).
The name of the submerchant (e.g., `EM_TEST_01`).
A description of the submerchant (e.g., `Submerchant created for testing purposes`).
The street address of the submerchant.
The postal code or zip code of the submerchant.
The city of the submerchant (e.g., `ABIDJAN`).
The two-letter ISO country code associated with the submerchant (e.g., `CI`).
This value is inherited from the merchant.
The currency code used by the submerchant (e.g., `XOF`).
This value is inherited from the merchant.
The phone number of the submerchant (e.g., `+2250707070707`).
A boolean flag indicating if the submerchant account is currently active (e.g., `true`).
The current operational status of the submerchant account (e.g., `live`).\
Possible values: `live`, `sandbox`, `testing`, `suspended`, `archived`
The defined activity level or performance tier of the submerchant (e.g., `top_performer`).
Possible values: `new`, `top_performer `, `low_performer`, `watch_list`
An object containing specific configuration settings and preferences for the submerchant.
This value is inherited from the merchant.
A list of users associated with the submerchant account (e.g., `[]`).
The date and time the submerchant was created, in ISO 8601 format (e.g., `2025-11-28T18:21:44.687Z`).
The date and time the submerchant was last updated, in ISO 8601 format (e.g., `2025-11-28T18:21:44.750Z`).
Legal information associated with the submerchant (e.g., `null`).
An internal code or reference number for the submerchant (e.g., `17C`).
Applicable policies or terms for the submerchant (e.g., `null`).
A boolean flag indicating if the submerchant is configured as an aggregator (e.g., `false`).
The ID of the merchant group to which the submerchant belongs (e.g., `tkExvG2obnQtXsrNMUw5d`).
```bash Submerchant Request theme={null}
curl --location 'https://api.hub2.io/submerchants' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: live' \
--header 'Content-Type: application/json' \
--data '{
"name": "EM_TEST_01",
"description": "Submerchant created for testing purposes",
"address": "RIVIERA BEVERLY HILLS",
"zipcode": "00225",
"city": "ABIDJAN",
"country": "CI",
"currency": "XAF",
"phoneNumber": "+2250707070707"
}'
```
```json Response theme={null}
{
"id": "wwIxF40ciS6NVQddoa5BQ",
"createdAt": "2025-11-27T18:37:55.033Z",
"updatedAt": "2025-11-28T00:00:00.803Z",
"name": "EM_TEST_01",
"description": "Submerchant created for testing purposes",
"country": "CI",
"currency": "XOF",
"members": [],
"preferences": {
"paymentWaveRestrictMsisdnEnabled": true
},
"address": "RIVIERA BEVERLY HILLS",
"zipcode": "00225",
"city": "ABIDJAN",
"status": "suspended",
"activity": "inactive_90_days",
"isActive": true,
"phoneNumber": null,
"isAggregator": false,
"policies": null,
"legalInfo": null,
"internalCode": "17B"
"merchantGroup": "tkExvG2obnQtXsrNMUw5d"
}
```
# List submerchants
Source: https://docs.hub2.io/api-reference/submerchants/list_submerchants
get /submerchants
# Update a submerchant
Source: https://docs.hub2.io/api-reference/submerchants/update-a-submerchant
patch /submerchants/{submerchantId}
# Create Transfer
Source: https://docs.hub2.io/api-reference/transfers/create-transfer
post /transfers
This endpoint allows you to make a transfer (PAY-OUT)
# Retrieve a Transfer
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfer
get /transfers/{id}
This endpoint allows you to fetch an existing transfer from our database
by providing its 'id'
# Retrieve a Transfer collection
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfer-collection
get /transfers
Return a list of Transfers, eventually filtered by criteria.
# Retrieve a transfer's balance
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfers-balance
get /transfers/{id}/balance
Fetch balance before and after transfer
# Retrieve a transfer's status
Source: https://docs.hub2.io/api-reference/transfers/retrieve-a-transfers-status
get /transfers/{id}/status
Retrieve the status for a transfer
# Create a webhook
Source: https://docs.hub2.io/api-reference/webhooks/create-a-webhook
post /webhooks
# Get a webhook
Source: https://docs.hub2.io/api-reference/webhooks/get-a-webhook
get /webhooks/{id}
# Get all webhooks
Source: https://docs.hub2.io/api-reference/webhooks/get-all-webhooks
get /webhooks
# Remove a webhook
Source: https://docs.hub2.io/api-reference/webhooks/remove-a-webhook
delete /webhooks/{id}
# Update a webhook
Source: https://docs.hub2.io/api-reference/webhooks/update-a-webhook
patch /webhooks/{id}
# Close payment links
Source: https://docs.hub2.io/api-reference/payment_links/close_payment_links
patch /payment-links/{id}/close
# Create Payment Links
Source: https://docs.hub2.io/api-reference/payment_links/create_payment_links
POST https://api.hub2.io/payment-links
This endpoint allows you to generate a secure payment link for receiving payments from your customers.
The Payment Links API allows you to accept payments from customers using a payment link URL. The payment link URL takes your shopper to an HUB2 hosted secure checkout page where they can make the payment with their preferred payment method.
## Request
Reference of the Purchase on your system (e.g., `EM_TEST_1006`)
The additional description text that appears under the title in the payment checkout page. (e.g., `Text displayed for the description`)
The payment amount (e.g., `500`).
Minimum amount for payment links is 500 XOF.
The payment currency. (e.g., `XOF`)
Country where the customers should pay (e.g., `CI`)
Specifies whether the payment link can be used once or multiple times. Default value is "single\_use" for the current version of the API. (e.g., `CI`)
The number of the customer associated with the purchaseReference. (e.g., `0707070707`)
Expiration date of the payment links (ISO 8601 format). Default value is 15mins (e.g., `2025-11-01T00:00:00.000Z`)
## Response
The unique Payment Links ID
The unique Payment Links URL
The current status of the Payment Links. Possible values: `active`, `completed`, `closed`, `expired`
Reference of the Purchase associated with the payment links
The payment amount
The currency of the payment (e.g., `XOF`, `XAF`)
Maximum payments allowed for a payment links.
Maximum payments attempts allowed for a payment links.
The date and time when the payment links was created
The date and time when the payment links was last updated
The date and time when the payment links will expire
The list of payment methods allowed for the payment link created
The URL to which the user is redirected upon a successful payment
The URL to which the user is redirected if the payment fails
The customer PhoneNumber set for the payment.
```bash Payment Links Request theme={null}
curl --location 'https://api.hub2.io/payment-links' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: live' \
--header 'Content-Type: application/json' \
--data '{
"purchaseReference": "EM_TEST_0016",
"description":"Payment Links are the fastest way to start accepting payments from your customers",
"amount": 100,
"currency": "XOF",
"type":"single_use",
"expirationDate": "2025-10-27T18:30:00",
"paymentMethods": [
"mobile_money"
],
"country": "CI",
"providers": [
"Orange",
"Moov",
"MTN"
]
}'
```
```json Response theme={null}
{
"id": "pl_PX0YZsb3iFg23vX7hWbcC",
"url": "https://pay.preprod.hub2.io/pl_PX0YZsb3iFg23vX7hWbcC",
"status": "active",
"purchaseReference": "EM_TEST_0016",
"amount": 100,
"currency": "XOF",
"maximumPayments": 1,
"maximumAttempts": 3,
"createdAt": "2025-10-27T09:17:52.927Z",
"updatedAt": "2025-10-27T09:17:52.927Z",
"expirationDate": "2025-10-27T18:30:00.000Z",
"operators": {
"mobile_money": [
"Orange",
"MTN",
"Moov"
]
},
"merchantId": "16",
"description": "Payment Links are the fastest way to start accepting payments from your customers. Simply input your currency and the amount you would like to charge your customer and a link will be generated. Links can be personalised.271025-09.13",
"successUrl": "https://hub2.io",
"failureUrl": "https://hub2.io",
"customerPhoneNumber": null,
"type": "single_use"
}
```
# List Payment Links
Source: https://docs.hub2.io/api-reference/payment_links/list_payment_links
get /payment-links
Retrieve a list of payment links with optional filtering
# Get Payment Link with Attempts
Source: https://docs.hub2.io/api-reference/payment_links/list_payment_links_with_attempts
get /payment-links/{id}
Retrieve detailed information about a payment link including payment attempts
# Create a delegated payment
Source: https://docs.hub2.io/api-reference/processing-only/make-a-delegated-payment
post /delegated/payments
This endpoint allows you to make a payment using your own provider credentials.
# Create a delegated Transfer
Source: https://docs.hub2.io/api-reference/processing-only/make-a-delegated-transfer
post /delegated/transfers
This endpoint allows you to make a transfer (PAY-OUT) using your own provider credentials.
# Create a delegated IRT transfer
Source: https://docs.hub2.io/api-reference/processing-only/make-a-delegated-transfer-irt
post /delegated/transfers/irt
Make an IRT transfer using your own provider credentials.
# Create a delegated synchronous payment
Source: https://docs.hub2.io/api-reference/processing-only/make-a-synchronous-delegated-payment
post /delegated/payments/sync
This endpoint allows you to make a synchronous payment using your own provider credentials.
# Delete a recipient by id
Source: https://docs.hub2.io/api-reference/recipients/delete-recipient
delete /recipient/{id}
# Get a recipient by id
Source: https://docs.hub2.io/api-reference/recipients/find-recipient
get /recipient/{id}
# Retrieve a list of recipients according to filters
Source: https://docs.hub2.io/api-reference/recipients/list-recipients
get /recipient
# Create a submerchant
Source: https://docs.hub2.io/api-reference/submerchants/create_submerchants
POST https://api.hub2.io/submerchants
This endpoint allows you to onboard a new submerchant and generate their unique ID for payment processing and transfers.
The Submerchant API allows you to create submerchant so that you can use their unique ID for transactions while ensuring compliance with regulatory requirements.
## Request Parameters
Name of the submerchant (e.g., `Submerchant Name`)
A description of the submerchant (e.g., `Submerchant description`).
This field can be used to give information regarding submerchant activity.
The street address of the submerchant (e.g., `RIVIERA BEVERLY HILLSt`).
The postal code or zip code of the submerchant (e.g., `00225`).
The city of the submerchant (e.g., `ABIDJAN`).
The phone number of the submerchant (e.g., `+2250707070707`).
## Response
**HTTP Status: 201 Created**
The unique ID for the submerchant (e.g., `wwIxF40ciS6NVQddoa5BQ`).
The name of the submerchant (e.g., `EM_TEST_01`).
A description of the submerchant (e.g., `Submerchant created for testing purposes`).
The street address of the submerchant.
The postal code or zip code of the submerchant.
The city of the submerchant (e.g., `ABIDJAN`).
The two-letter ISO country code associated with the submerchant (e.g., `CI`).
This value is inherited from the merchant.
The currency code used by the submerchant (e.g., `XOF`).
This value is inherited from the merchant.
The phone number of the submerchant (e.g., `+2250707070707`).
A boolean flag indicating if the submerchant account is currently active (e.g., `true`).
The current operational status of the submerchant account (e.g., `live`).\
Possible values: `live`, `sandbox`, `testing`, `suspended`, `archived`
The defined activity level or performance tier of the submerchant (e.g., `top_performer`).
Possible values: `new`, `top_performer `, `low_performer`, `watch_list`
An object containing specific configuration settings and preferences for the submerchant.
This value is inherited from the merchant.
A list of users associated with the submerchant account (e.g., `[]`).
The date and time the submerchant was created, in ISO 8601 format (e.g., `2025-11-28T18:21:44.687Z`).
The date and time the submerchant was last updated, in ISO 8601 format (e.g., `2025-11-28T18:21:44.750Z`).
Legal information associated with the submerchant (e.g., `null`).
An internal code or reference number for the submerchant (e.g., `17C`).
Applicable policies or terms for the submerchant (e.g., `null`).
A boolean flag indicating if the submerchant is configured as an aggregator (e.g., `false`).
The ID of the merchant group to which the submerchant belongs (e.g., `tkExvG2obnQtXsrNMUw5d`).
```bash Submerchant Request theme={null}
curl --location 'https://api.hub2.io/submerchants' \
--header 'ApiKey: [REDACTED]' \
--header 'MerchantId: [REDACTED]' \
--header 'Environment: live' \
--header 'Content-Type: application/json' \
--data '{
"name": "EM_TEST_01",
"description": "Submerchant created for testing purposes",
"address": "RIVIERA BEVERLY HILLS",
"zipcode": "00225",
"city": "ABIDJAN",
"country": "CI",
"currency": "XAF",
"phoneNumber": "+2250707070707"
}'
```
```json Response theme={null}
{
"id": "wwIxF40ciS6NVQddoa5BQ",
"createdAt": "2025-11-27T18:37:55.033Z",
"updatedAt": "2025-11-28T00:00:00.803Z",
"name": "EM_TEST_01",
"description": "Submerchant created for testing purposes",
"country": "CI",
"currency": "XOF",
"members": [],
"preferences": {
"paymentWaveRestrictMsisdnEnabled": true
},
"address": "RIVIERA BEVERLY HILLS",
"zipcode": "00225",
"city": "ABIDJAN",
"status": "suspended",
"activity": "inactive_90_days",
"isActive": true,
"phoneNumber": null,
"isAggregator": false,
"policies": null,
"legalInfo": null,
"internalCode": "17B"
"merchantGroup": "tkExvG2obnQtXsrNMUw5d"
}
```
# List submerchants
Source: https://docs.hub2.io/api-reference/submerchants/list_submerchants
get /submerchants
# Update a submerchant
Source: https://docs.hub2.io/api-reference/submerchants/update-a-submerchant
patch /submerchants/{submerchantId}