# Create a Payment

> Full reference for POST /api/payments. Every field, every response, and copy-paste examples in cURL, Node.js, and Python.

Source: https://mutopay.com/docs/create-payment/
Language: en

---

`POST https://mutopay.com/api/payments` creates a payment link. Authenticate with a channel API key in the `X-API-Key` header.

## Request

```http
POST /api/payments HTTP/1.1
Host: mutopay.com
X-API-Key: ep_live_4b8f...
Content-Type: application/json
```

### Body

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_usd` | number | yes* | Amount in USD. Required unless `amount_original` + `currency` are supplied. There is a per-channel minimum (typically a few dollars), recompute hourly. Smaller values return 400 with the exact threshold in the `message`. |
| `amount_original` | number | no | Amount in the original currency. Used with `currency`. |
| `currency` | string | no | ISO-4217 code (`EUR`, `GBP`, `JPY`, ...). Defaults to `USD`. |
| `description` | string | no | Human-readable description shown to the customer on the payment page. |
| `external_id` | string | no | Your order/invoice ID. Returned on webhooks for reconciliation. |
| `callback_url` | string | no | Where to send the customer after payment completes. |
| `customer_email` | string | no | For receipt emails and dispute resolution. |
| `expires_at` | ISO-8601 string | no | Override the default 30-minute expiry. Max 7 days. |
| `metadata` | object | no | Arbitrary JSON metadata, echoed back on webhooks. ≤ 4 KB. |

## Response, 201 Created

```json
{
  "id": "pay_abc123",
  "status": "pending",
  "url": "https://mutopay.com/pay/pay_abc123",
  "amount_usd": 25.00,
  "currency": "USD",
  "description": "Order #1042",
  "external_id": "order_1042",
  "expires_at": "2026-04-21T14:30:00Z",
  "created_at": "2026-04-21T14:00:00Z"
}
```

Redirect your customer to `url`.

## Examples

### cURL

```bash
curl -X POST https://mutopay.com/api/payments \
  -H "X-API-Key: $MUTOPAY_CHANNEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usd": 25.00,
    "description": "Order #1042",
    "external_id": "order_1042",
    "callback_url": "https://yourstore.com/thanks",
    "metadata": {"user_id": "u_88124", "tier": "pro"}
  }'
```

### Node.js (fetch)

```js
const res = await fetch('https://mutopay.com/api/payments', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.MUTOPAY_CHANNEL_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount_usd: 25.00,
    description: 'Order #1042',
    external_id: 'order_1042',
    callback_url: 'https://yourstore.com/thanks',
  }),
});

if (!res.ok) throw new Error(`MutoPay error: ${res.status}`);
const payment = await res.json();
// Redirect the customer:
//   res.redirect(payment.url);
```

### Python (requests)

```python
import os
import requests

res = requests.post(
    "https://mutopay.com/api/payments",
    headers={
        "X-API-Key": os.environ["MUTOPAY_CHANNEL_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "amount_usd": 25.00,
        "description": "Order #1042",
        "external_id": "order_1042",
        "callback_url": "https://yourstore.com/thanks",
    },
    timeout=10,
)
res.raise_for_status()
payment = res.json()
# Redirect the customer to payment["url"]
```

## Non-USD pricing

Pass `amount_original` + `currency` for merchants pricing in EUR/GBP/etc. The FX rate is captured at creation time and returned in webhook payloads as `fx_rate`.

```json
{
  "amount_original": 25.00,
  "currency": "EUR",
  "description": "Abonnement Pro"
}
```

MutoPay quotes the customer in USD equivalent, then settles in your preferred stablecoin.

## Errors

| Status | Body | Meaning |
|---|---|---|
| 400 | `{"error": "invalid_request", "message": "..."}` | Validation error (see `message`). |
| 400 | `{"error": "invalid_request", "message": "Minimum invoice on this channel is $N.NN."}` | `amount_usd` is below the channel's minimum. Show the value from `message` to the customer or bump your minimum order size. |
| 401 | `{"error": "unauthorized"}` | Missing or invalid API key. |
| 403 | `{"error": "merchant suspended"}` | Your account is suspended. Contact support. |
| 429 | `{"error": "rate_limited"}` | Too many requests. Retry with exponential backoff. |

## See also

- [Webhooks](/docs/webhooks/): what you get back after the customer pays.
- [Payment statuses](/docs/payment-statuses/): state machine reference.
