# NabooPay API v2 reference

Base URL: `https://api.naboopay.com/api/v2`

Every request needs `Authorization: Bearer <API_KEY>`. Requests with a body need `Content-Type: application/json`. All amounts are integer XOF. All timestamps are RFC 3339 (`2025-01-15T10:35:00Z`).

## Contents

- [Scopes](#scopes)
- [Transactions](#transactions): create, get, list, cancel
- [Refunds](#refunds)
- [Payouts](#payouts): create, get, list
- [Accounts](#accounts)
- [Errors](#errors)

## Scopes

| Scope | Allows |
|---|---|
| `checkout` | Create, read, list and cancel transactions; refund |
| `read` | Read and list transactions; read payouts; read accounts |
| `write` | Create and cancel transactions; refund |
| `readAndWrite` | Everything in `read` and `write` for transactions; read accounts |
| `payout` | Create, read and list payouts |
| `management` | Refund; read accounts |

The scope string is `readAndWrite`, camel case.

| Endpoint | Accepted scopes |
|---|---|
| `POST /transactions` | `checkout`, `write`, `readAndWrite` |
| `GET /transactions`, `GET /transactions/{order_id}` | `checkout`, `read`, `readAndWrite` |
| `DELETE /transactions/{order_id}` | `checkout`, `write`, `readAndWrite` |
| `GET /refund/{order_id}` | `checkout`, `write`, `readAndWrite`, `management` |
| `POST /payouts` | `payout` |
| `GET /payouts`, `GET /payouts/{order_id}` | `payout`, `read` |
| `GET /accounts`, `GET /accounts/stats`, `GET /accounts/{id}` | `read`, `readAndWrite`, `management` |

## Transactions

### Create a transaction

`POST /transactions`

| Field | Type | Required | Notes |
|---|---|---|---|
| `method_of_payment` | string[] | yes | At least one of `wave`, `orange_money`. These are the methods offered on the checkout page. Each must be allowed for the organisation. |
| `products` | object[] | yes | At least one. |
| `products[].name` | string | yes | |
| `products[].price` | integer | yes | Unit price in XOF. |
| `products[].quantity` | integer | yes | Must be non-zero. |
| `products[].description` | string | no | |
| `customer` | object | no | `first_name`, `last_name`, `phone`. Recommended: it makes transactions searchable by customer. |
| `success_url` | string | no | Where the customer goes after paying. Defaults to a NabooPay page. No percent-encoded characters. |
| `error_url` | string | no | Where the customer goes on failure. Same rules. |
| `fees_customer_side` | boolean | no | `true`: fees added to what the customer pays. `false` (default): fees deducted from what the merchant receives. |
| `is_escrow` | boolean | no | Hold funds until released. Requires escrow to be enabled for the organisation. |
| `is_merchant` | boolean | no | Default `false`. |

The total is the sum of `price × quantity` and must be greater than 10.

Response `200`:

```json
{
  "order_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 5000,
  "method_of_payment": ["wave", "orange_money"],
  "currency": "XOF",
  "created_at": "2025-01-15T10:30:00Z",
  "transaction_status": "pending",
  "customer": { "first_name": "Awa", "last_name": "Diop", "phone": "+221771234567", "created_at": "2025-01-15T10:30:00Z" },
  "is_escrow": false,
  "is_merchant": false,
  "checkout_url": "https://checkout.naboopay.com/checkout/550e8400-e29b-41d4-a716-446655440000"
}
```

`order_id` is a UUID generated by NabooPay. Store it. Redirect the customer to `checkout_url`.

Errors specific to this endpoint:

| Status | `error` |
|---|---|
| 400 | `Invalid request body` |
| 400 | `Add minimum one payment method` |
| 400 | `Add minimum one product` |
| 400 | `Total price must be greater than 10 and the quantity must be non-zero` |
| 400 | `Amount is outside the allowed transaction limits, ...` |
| 400 | `Invalid url` |
| 400 | `URLs cannot contain encoded special characters (e.g., spaces). ...` |
| 403 | `Escrow is not enabled for this organization, ...` |
| 403 | `<method> payment is not allowed for this organization, ...` |
| 403 | `Monthly transaction limit of 200000 XOF reached for unverified organizations. ...` |
| 403 | `Complete your business profile (business sector) before making new requests: ...` |
| 503 | `Transaction creation is temporarily disabled` |
| 503 | `Unable to prepare merchant account, please try again shortly` |

### Get a transaction

`GET /transactions/{order_id}`

Response `200`:

```json
{
  "order_id": "550e8400-e29b-41d4-a716-446655440000",
  "amount": 5000,
  "fees": 100,
  "method_of_payment": ["wave", "orange_money"],
  "selected_payment_method": "wave",
  "currency": "XOF",
  "transaction_status": "paid",
  "customer": { "first_name": "Awa", "last_name": "Diop", "phone": "+221771234567", "created_at": "2025-01-15T10:30:00Z" },
  "products": [{ "name": "T-shirt", "price": 5000, "quantity": 1 }],
  "is_escrow": false,
  "is_merchant": false,
  "fees_customer_side": false,
  "success_url": "https://example.com/orders/123/thanks",
  "error_url": "https://example.com/orders/123/failed",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:35:00Z",
  "paid_at": "2025-01-15T10:35:00Z"
}
```

`transaction_status` is one of `pending`, `paid`, `paid_and_blocked`, `cancelled`, `refunded`. `selected_payment_method` is the method the customer actually used and is empty until they pay. Ignore `paid_at` unless the status is `paid` or `paid_and_blocked`.

`404 Transaction not found` if the ID is unknown.

### List transactions

`GET /transactions`

| Query parameter | Notes |
|---|---|
| `page` | Default `1`. |
| `limit` | Default `20`, maximum `100`. |
| `status` | `pending`, `paid`, `paid_and_blocked`, `cancelled`, `refunded`. |
| `payment_method` | `wave` or `orange_money` (the method used to pay). |
| `customer_phone` | Exact match. |
| `min_amount`, `max_amount` | Integers, inclusive. |
| `start_date`, `end_date` | RFC 3339, filter on `created_at`. URL-encode the `+` in time zone offsets, or use `Z`. |
| `is_escrow`, `is_merchant` | `true` or `false`. |
| `include_deleted` | `true` to include cancelled (soft deleted) transactions. |
| `search` | Free text search. |

Response `200`:

```json
{
  "transactions": [ { "...": "same shape as Get a transaction" } ],
  "pagination": { "page": 1, "limit": 20, "total_count": 57, "total_pages": 3 }
}
```

### Cancel a transaction

`DELETE /transactions/{order_id}`

Cancels a transaction that has not been paid. Its status becomes `cancelled`.

Response `200`: `{ "message": "Transaction deleted successfully. Status changed to cancelled" }`

| Status | `error` |
|---|---|
| 400 | `Cannot delete a paid transaction` (use the refund endpoint instead) |
| 400 | `Transaction is already deleted` |
| 403 | `You don't have permission to delete this transaction` |
| 404 | `Transaction not found` |

## Refunds

`GET /refund/{order_id}`

Refunds the full amount of a paid transaction to the customer through the method they paid with. The verb is `GET` and there is no body.

- The transaction must be `paid` or `paid_and_blocked`.
- The merchant's main account balance must cover the transaction amount.
- Partial refunds are not supported.
- A refund cannot be undone.

Response `200`: `{ "message": "Refund successful" }`

| Status | `error` |
|---|---|
| 400 | `This Transaction cannot be refund` (status is pending, cancelled, or already refunded) |
| 400 | `Insufficient balance` |
| 404 | `Transaction not found` |
| 500 | `Failed to refund` / `The refund have failed in wave level` |

After a `500`, call `GET /transactions/{order_id}` and check whether the status is `refunded` before trying again.

## Payouts

### Create a payout

`POST /payouts`

Sends money from the merchant's main account to a mobile money wallet. Payouts are processed synchronously and cannot be reversed.

| Field | Type | Required | Notes |
|---|---|---|---|
| `selected_payment_method` | string | yes | `wave` or `orange_money`. |
| `amount` | integer | yes | XOF, greater than 10. |
| `recipient` | object | yes | |
| `recipient.first_name` | string | no | |
| `recipient.last_name` | string | no | |
| `recipient.phone` | string | yes in practice | The wallet number, international format, for example `+221771234567`. |
| `reason` | string | no | Free text. |

Response `200`:

```json
{
  "_id": "65a4f0c2e4b0a1b2c3d4e5f6",
  "organization_id": "65a4f0c2e4b0a1b2c3d4e5f0",
  "order_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "selected_payment_method": "wave",
  "amount": 10000,
  "fees": 0,
  "currency": "XOF",
  "recipient": { "first_name": "Amadou", "last_name": "Diallo", "phone": "+221771234567", "created_at": "2025-01-15T10:30:00Z" },
  "payout_status": "completed",
  "reason": "Invoice 42",
  "provider_reference": "",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:02Z",
  "paid_at": "2025-01-15T10:30:02Z",
  "is_deleted": false
}
```

Always read `payout_status` from the response; a `200` does not by itself mean the money was sent:

- `completed`: sent.
- `pending`: the amount is above 2,000,000 XOF and is waiting for manual review by NabooPay. Poll `GET /payouts/{order_id}` later.

| Status | `error` |
|---|---|
| 400 | `Invalid request` |
| 400 | `Invalid request body: <field> is required, <field> must be one of [...]` |
| 400 | `Invalid amount` (10 or less) |
| 400 | `Insufficient balance` |
| 400 | `Unsupported payment method` |
| 403 | `Payout is not enabled for this organization, ...` |
| 403 | `Complete your business profile (business sector) before making new requests: ...` |
| 409 | `A payout is already in progress. Please wait.` |
| 500 | `Failed to withdraw` / `The withdraw have failed in wave level` / `The withdraw have failed in orange level` |
| 503 | `Payout is temporarily disabled`, or a message asking to use the other payment method because the provider is unavailable |

The `409` is a lock per organisation: only one payout runs at a time. Send payouts sequentially. If you get a `409`, wait a few seconds and retry that same payout.

After a timeout or `5xx`, do not resend. List payouts filtered by `recipient_phone` and check whether one with the same amount was just created and what its status is.

### Get a payout

`GET /payouts/{order_id}`

Response `200`: the payout object, with the same fields as above except `_id` and `organization_id`. `payout_status` is one of `pending`, `completed`, `failed`, `cancelled`.

`404 Payout not found`, `403 You don't have permission to view this payout`.

### List payouts

`GET /payouts`

| Query parameter | Notes |
|---|---|
| `page` | Default `1`. |
| `limit` | Default `20`, maximum `100`. |
| `status` | `pending`, `completed`, `failed`, `cancelled`. |
| `payment_method` | `wave` or `orange_money`. |
| `recipient_phone` | Exact match. |
| `min_amount`, `max_amount` | Integers, inclusive. |
| `start_date`, `end_date` | RFC 3339. |
| `include_deleted` | `true` or `false`. |
| `search` | Free text search. |

Response `200`:

```json
{
  "payouts": [ { "...": "payout object" } ],
  "pagination": { "page": 1, "limit": 20, "total_count": 4, "total_pages": 1 }
}
```

## Accounts

### List accounts

`GET /accounts`

Response `200`: an array (not wrapped in an object).

```json
[
  {
    "id": "507f1f77bcf86cd799439011",
    "name": "main",
    "type": "current",
    "organization_id": "65a4f0c2e4b0a1b2c3d4e5f0",
    "balance": 125000,
    "currency": "XOF",
    "is_active": true,
    "created_at": "2024-06-01T09:00:00Z",
    "updated_at": "2025-01-15T10:35:00Z"
  }
]
```

The account named `main` is the one payments are credited to and payouts and refunds are debited from. `balance` is in XOF.

### Account stats

`GET /accounts/stats`

Response `200`: `{ "total_balance": 125000, "account_count": 1, "active_account_count": 1 }`

### Get an account

`GET /accounts/{id}`

`id` is the account's `id` from the list. Response `200`: a single account object.

`400 Invalid account ID`, `404 Account not found`, `403 Access denied`.

## Errors

Every error response is JSON with a single field:

```json
{ "error": "Invalid API key" }
```

Errors returned by any endpoint:

| Status | `error` | Cause |
|---|---|---|
| 401 | `Invalid API key` | Unknown key, or a malformed `Authorization` header. |
| 401 | `Organization is not active` | The organisation has been deactivated. |
| 403 | `You don't have permission to perform this action` | The key lacks a scope this endpoint accepts. |
| 403 | `Organization is blocked, please contact naboopay contact@naboopay.com` | |
| 429 | rate limit message | More than 100 requests in 60 seconds to one endpoint from the same IP. |
| 500 | `Internal server error` | |

Match on the status code, not on the exact wording of `error`; the wording can change. Log the message and show it to the developer.
