# MutoPay API

Version: 1.0.0
Base URL: https://mutopay.com

Crypto payment gateway, accept any token, receive your preferred stablecoin.

## Authentication

MutoPay uses two API keys, each for a different purpose:

| Key | Prefix | Header | Scope |
|-----|--------|--------|-------|
| Channel API key | `ep_` | `X-API-Key` | Create payments, one key per integration (WooCommerce, mobile app, etc.) |
| Master API key | `msk_` | `Authorization: Bearer` | Manage channels, settlement, and merchant settings |

### Creating payments

Use a **channel API key** in the `X-API-Key` header. Each channel has its own key so you can revoke or rotate one integration without affecting others.

```http
POST /api/payments
X-API-Key: ep_<channel_api_key>
```

Get or create channel keys from [Settings → Channels](/dashboard/settings).

### Managing channels and settings

Use a **master API key** as a Bearer token. This grants access to all `/api/merchant/*` endpoints.

```http
GET /api/merchant/channels
Authorization: Bearer msk_<master_api_key>
```

Generate a master key from [Settings → Master API Key](/dashboard/settings).

## Payment Flow

1. **Create** a payment via `POST /api/payments` with the amount and optional metadata
2. Redirect your customer to `https://mutopay.com/pay/{id}`, the hosted payment page handles token selection, wallet connection, and on-chain execution
3. **Poll** `GET /api/payments/{id}/status` for real-time status updates, or wait for a webhook

### Optional: channel attribution

Pass `channel_id` when creating a payment to attribute it to a specific channel. The channel's settlement override (token, chain, wallet address) will be used for that payment instead of the account default.

### Payment Statuses

| Status | Meaning |
|--------|---------|
| `pending` | Created, waiting for customer to start |
| `awaiting_payment` | Customer selected token, waiting for payment |
| `confirming` | Transaction submitted, verifying on-chain |
| `processing` | Swap/bridge order executing |
| `completed` | Payment received successfully |
| `failed` | Payment failed (see `failure_reason`) |
| `expired` | Payment expired before completion |
| `underpaid` | Transfer amount below required threshold |

## Webhooks

Configure your webhook URL in the merchant dashboard. Payloads are signed with HMAC-SHA256.

Verify the `X-MutoPay-Signature` header:

```
signature = HMAC-SHA256(webhook_secret, request_body)
expected  = "sha256=" + hex(signature)
```

Events: `payment.completed`, `payment.failed`, `payment.expired`, `payment.underpaid`, `payment.kyc_required`, `payment.needs_manual_check`

### Webhook Payload

```json
{
  "event": "payment.completed",
  "payment_id": "pay_abc123",
  "status": "completed",
  "amount_usd": 54.23,
  "amount_original": 50,
  "fx_rate": 1.0846,
  "currency": "EUR",
  "src_token": "USDC",
  "src_chain_id": "56",
  "src_amount": null,
  "dest_token": "USDC",
  "dest_chain_id": "137",
  "dest_amount": "54230000",
  "dest_decimals": 6,
  "external_id": "order_123",
  "tx_hash": "0x...",
  "failure_reason": null,
  "completed_at": "2026-04-01T12:00:00Z",
  "timestamp": "2026-04-01T12:00:01Z"
}
```

`dest_amount` is in raw token base units. Divide by `10^dest_decimals` for human-readable (e.g. `54230000 / 10^6 = 54.23 USDC`).

Respond with a 2xx status. Failed deliveries are retried up to 5 times with exponential backoff (1m, 5m, 30m, 2h, 12h).

## Endpoints

## Channels

### GET `/api/merchant/channels` — List channels

Returns all channels for the authenticated merchant, including webhook secrets and settlement override fields. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Responses:**

- `200` Channel list

```json
{
  "channels": [
    null
  ]
}
```
- `401` Unauthorized

### POST `/api/merchant/channels` — Create a channel

Creates a new manual channel. Returns the channel object and the full API key (shown **once**: store it immediately). Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Request body:**

**Responses:**

- `201` Channel created, store the `api_key` now, it won't be shown again

```json
{
  "api_key": "ep_abc..."
}
```
- `400` Validation error
- `401` Unauthorized

### GET `/api/merchant/channels/{id}` — Get a channel

Returns full channel detail including webhook secret and settlement override. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` Channel detail
- `401` Unauthorized
- `404` Channel not found

### PUT `/api/merchant/channels/{id}` — Update channel labels

Updates the channel's dashboard label (`name`) and/or customer-facing display fields (`display_name`, `tagline`). Pass `null` for display fields to fall back to account branding. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Request body:**

```json
{
  "name": "<Internal dashboard label>",
  "display_name": "<Customer-facing name (null = use merchant business name)>",
  "tagline": "<Customer-facing tagline (null = use merchant page tagline)>"
}
```

**Responses:**

- `200` Updated
- `401` Unauthorized
- `404` Channel not found

### PATCH `/api/merchant/channels/{id}/webhook-url` — Update webhook URL

Updates the channel's webhook URL independently of other fields. Pass `null` to remove it. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Request body:**

```json
{
  "webhook_url": "https://yourserver.com/webhooks/mutopay"
}
```

**Responses:**

- `200` Updated

```json
{
  "webhook_url": "string"
}
```
- `401` Unauthorized
- `404` Channel not found

### PUT `/api/merchant/channels/{id}/settlement` — Set settlement override

Sets a per-channel settlement override (token + chain + wallet). All three fields are required and applied atomically. Payments created with this `channel_id` will use these `dest_*` values instead of the account default. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Request body:**

**Responses:**

- `200` Override set

```json
{
  "preferred_token": "string",
  "preferred_chain_id": "string",
  "wallet_address": "string"
}
```
- `400` Validation error (unsupported token/chain combination)
- `401` Unauthorized
- `404` Channel not found

### DELETE `/api/merchant/channels/{id}/settlement` — Remove settlement override

Clears the per-channel settlement override. The channel falls back to the account-level default for all future payments. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` Override removed
- `401` Unauthorized
- `404` Channel not found

### POST `/api/merchant/channels/{id}/regenerate-api-key` — Rotate channel API key

Generates a new API key for the channel, immediately invalidating the old one. Returns the full key **once**: store it immediately. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` New key, store immediately

```json
{
  "api_key": "ep_abc...",
  "api_key_prefix": "ep_abc12345"
}
```
- `401` Unauthorized
- `404` Channel not found

### POST `/api/merchant/channels/{id}/revoke` — Revoke a channel

Permanently revokes the channel's API key. The channel record is retained but status changes to `revoked` and the key stops working immediately. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` Channel revoked
- `401` Unauthorized
- `404` Channel not found

### POST `/api/merchant/channels/{id}/test-webhook` — Send a test webhook

Fires a `payment.completed` test payload to the channel's webhook URL. Accepts a master API key (`msk_`) or a JWT browser session.

**Auth:** MasterKeyAuth, JwtAuth

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` Result of the test delivery

```json
{
  "delivered": false,
  "status_code": 0,
  "error": "string"
}
```
- `401` Unauthorized
- `404` Channel not found or no webhook URL set

## Payments

### POST `/api/payments` — Create a payment

Creates a new payment request. Returns a payment object including the `id`, redirect your customer to `/pay/{id}` to complete payment.

**Auth:** ApiKeyAuth

**Request body:**

```json
{
  "amount": 0,
  "amount_usd": 0,
  "currency": "USD",
  "description": "string",
  "callback_url": "string",
  "external_id": "string",
  "metadata": {},
  "expires_in_minutes": 0,
  "channel_id": "string"
}
```

**Responses:**

- `201` Payment created successfully
- `400` Validation error
- `401` Invalid or missing API key

### POST `/api/payments/headless` — Create a payment (headless, deposit-based)

Creates a payment, selects the caller-provided source token/chain, and returns a ready-to-use deposit address + exact amount the customer must send. One round trip: no hosted `/pay/{id}` UI and no order-signing required.

Supports **deposit-based routes only**:
- **direct** (same token, same chain), the customer sends to the merchant's wallet
- **swap / bridge deposit**: the customer sends to an intake address and the settlement token arrives in the merchant's wallet

If the only available route for the given src→dest pair requires the customer to sign an on-chain transaction or typed order, the endpoint returns 422 and the caller should fall back to the hosted flow.

After calling this endpoint, the caller polls `GET /api/payments/{id}/status` until terminal. Swap/bridge deposits are picked up automatically by the background monitor; direct routes can optionally `POST /{id}/confirm` with the tx hash once the customer broadcasts.

**Auth:** ApiKeyAuth

**Request body:**

**Responses:**

- `201` Payment created and deposit-ready
- `400` Validation error or unsupported destination token/chain
- `401` Invalid or missing API key
- `422` No deposit-capable route for this src→dest pair

### GET `/api/payments/{id}` — Get payment details

Retrieves the full details of a payment. No authentication required, payment IDs are unguessable.

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` Payment details
- `404` Payment not found

### GET `/api/payments/{id}/status` — Poll payment status

Returns the current payment status. Performs inline on-chain verification for in-progress payments, so you get real-time updates without waiting for the background monitor.

**Parameters:**

| Name | In | Required | Type | Description |
|------|----|----------|------|-------------|
| `id` | path | yes | string |  |

**Responses:**

- `200` Current payment status
- `404` Payment not found
