> For the complete documentation index, see [llms.txt](https://docs.madhousewallet.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.madhousewallet.com/quickstart/api-reference.md).

# API Reference

**Base URL**: `https://business.madhousewallet.com`\
**Authentication**: `Authorization: Bearer mw_live_<keyId>_<secret>`\
**Content-Type**: `application/json`

Interactive API docs are available at <https://developer.madhousewallet.com>.

All successful responses are returned as JSON. Rate limits are per API key.

***

## MCP Server

**Endpoint**: [`https://mcp.madhousewallet.com/`](https://mcp.madhousewallet.com/)

Madhouse Wallet offers a hosted [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that exposes the payouts API as a set of tools your AI agents can call directly. Instead of hand-wiring HTTP requests, an MCP-compatible client (Claude, Cursor, or any agent framework) can discover and invoke wallet actions — fetching quotes, managing recipients, and initiating transfers — through a single connection.

Use it to let AI agents:

* Look up account requirements and create recipients
* Request locked FX quotes
* Initiate and track USDC off-ramp transfers
* Programmatically orchestrate payouts as part of an agentic workflow

Authentication uses the same `mw_live_<keyId>_<secret>` API keys described above.

For setup instructions, the full tool catalog, and client configuration, see <https://mcp.madhousewallet.com/>.

***

## Recipients

### GET /api/payouts/recipients

List all recipients for the authenticated account.

**Rate limit**: 30 req/min

**Response:**

```json
{
  "data": [
    {
      "id": 123456789,
      "currency": "KES",
      "accountHolderName": "Jane Mwangi",
      "type": "kenya_local"
    }
  ]
}
```

***

### POST /api/payouts/recipients

Create a new payout recipient.

**Rate limit**: 30 req/min

**Request body:**

```json
{
  "currency": "KES",
  "type": "kenya_local",
  "accountHolderName": "Jane Mwangi",
  "details": {
    "legalType": "PRIVATE",
    "accountNumber": "1234567890"
  }
}
```

Use `GET /api/payouts/account-requirements` first to determine the correct `type` and required `details` fields for each currency.

**Response:**

```json
{
  "id": 123456789,
  "currency": "KES",
  "accountHolderName": "Jane Mwangi"
}
```

***

### GET /api/payouts/recipients/:id

Fetch a single recipient by ID.

**Rate limit**: 30 req/min

***

### DELETE /api/payouts/recipients/:id

Delete a recipient. Returns `409 Conflict` if a transfer for this recipient is in a non-terminal status.

**Rate limit**: 30 req/min

***

## Account Requirements

### GET /api/payouts/account-requirements

Fetch required fields for creating a recipient in a target currency.

**Rate limit**: 60 req/min

**Query parameters:**

| Parameter      | Type   | Description                       |
| -------------- | ------ | --------------------------------- |
| `source`       | string | Source currency (e.g. `USD`)      |
| `target`       | string | Target fiat currency (e.g. `KES`) |
| `sourceAmount` | number | Amount in source currency         |

**Response** (array of requirement types):

```json
[
  {
    "type": "kenya_local",
    "title": "Local bank account",
    "fields": [
      {
        "name": "Account number",
        "group": [
          {
            "key": "accountNumber",
            "type": "text",
            "required": true,
            "minLength": 10,
            "maxLength": 10,
            "validationRegexp": "^\\d{10}$",
            "refreshRequirementsOnChange": false
          }
        ]
      }
    ]
  }
]
```

Fields with `refreshRequirementsOnChange: true` (e.g. `legalType`) require a follow-up POST to this endpoint with the current form state to receive updated field lists.

***

### POST /api/payouts/account-requirements

Refresh requirements after a `refreshRequirementsOnChange` field changes.

**Rate limit**: 60 req/min

**Request body:**

```json
{
  "type": "kenya_local",
  "details": {
    "legalType": "BUSINESS"
  }
}
```

***

## Quotes

### GET /api/payouts/quote

Get a locked FX quote. Valid for **5 minutes**.

**Rate limit**: 20 req/min

**Query parameters:**

| Parameter        | Type   | Description                       |
| ---------------- | ------ | --------------------------------- |
| `targetCurrency` | string | Target fiat currency (e.g. `KES`) |
| `sourceAmount`   | number | USD amount to send                |

**Response:**

```json
{
  "quoteId": "a1b2c3d4-e5f6-...",
  "txFee": 2,
  "netUsdAmount": 198,
  "eurAmount": 183.12,
  "usdAmount": 200,
  "quote": {
    "sourceCurrency": "EUR",
    "targetCurrency": "KES",
    "paymentOptions": [
      {
        "payIn": "BALANCE",
        "targetAmount": 28450.00,
        "fee": { "total": 1.20, "currency": "EUR" },
        "disabled": false
      }
    ],
    "deliveryEstimate": "Within hours"
  }
}
```

Pass `quoteId` to `POST /api/payouts/transfer` within the 5-minute window.

***

## Transfers

### POST /api/payouts/transfer

Initiate a USDC off-ramp transfer.

**Rate limit**: 5 req/min

**Request body:**

| Field           | Type   | Required | Description                                             |
| --------------- | ------ | -------- | ------------------------------------------------------- |
| `quote_id`      | string | Yes      | From `GET /api/payouts/quote`                           |
| `source_wallet` | string | Yes      | Ethereum address (0x...) sending the USDC               |
| `amount`        | number | Yes      | USD amount (must match quote ±$0.01)                    |
| `recipientId`   | number | Yes      | From `POST /api/payouts/recipients`                     |
| `customer_uuid` | string | No       | Your own idempotency key (prevents duplicates on retry) |

**Response:**

```json
{
  "transfer_id": "txn_abc123",
  "escrow_wallet": "0xDepositAddress...",
  "wallet_address": "0xYourSourceWallet",
  "amount": 200,
  "currency": "KES",
  "status": "pending",
  "expires_at": "2026-03-29T12:10:00Z"
}
```

Send exactly `amount` USDC from `source_wallet` to `escrow_wallet` before `expires_at`.

**Error codes:**

| Code  | Meaning                                 |
| ----- | --------------------------------------- |
| `400` | Invalid input or stale/mismatched quote |
| `403` | Recipient not owned by this account     |
| `409` | Duplicate `customer_uuid`               |
| `429` | Rate limit exceeded                     |

***

### POST /api/payouts/transfer/cancel

Cancel a transfer before USDC has been sent.

**Rate limit**: 20 req/min

**Request body:**

```json
{ "transfer_id": "txn_abc123" }
```

Returns `409` if the transfer has already received a deposit.

***

### GET /api/payouts/transfer/:id

Get transfer status from the database snapshot.

**Rate limit**: 30 req/min

***

### GET /api/payouts/transfer-status/:id

Get transfer status with **live** quote data and recipient details fetched in real time.

**Rate limit**: 30 req/min

**Response:**

```json
{
  "transfer_id": "txn_abc123",
  "status": "completed",
  "amount": 200,
  "currency": "KES",
  "recipientId": 123456789,
  "wallet_address": "0xYourSourceWallet",
  "created_at": "2026-03-29T12:00:00Z",
  "updated_at": "2026-03-29T12:08:00Z"
}
```

***

## Rate Limits

Rate limits are **per API key**. Headers are included on every response:

```
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 1743254460
```

When exceeded, the API returns `429 Too Many Requests`. Back off and retry after `X-RateLimit-Reset`.

***

## Errors

All errors follow the format:

```json
{ "error": "Description of what went wrong" }
```

| Status | Meaning                                        |
| ------ | ---------------------------------------------- |
| `400`  | Bad request — check input parameters           |
| `401`  | Invalid or missing API key                     |
| `403`  | Forbidden — resource not owned by this account |
| `404`  | Resource not found                             |
| `409`  | Conflict — duplicate or in-flight transfer     |
| `429`  | Rate limit exceeded                            |
| `500`  | Internal server error — contact support        |
