# Merchant API

The merchant API is what your shop or backend calls: create a payment, send the customer to the hosted checkout,
and get a signed webhook when it's paid. Payouts from the hot wallet use the same key.

The machine-readable reference is [`openapi.yaml`](https://coldtill.com/openapi.yaml) (OpenAPI 3.1), rendered at
[coldtill.com/docs/api](https://coldtill.com/docs/api). Import it into Postman, Insomnia or a client generator.
New here? Start with the guide [Accept a payment from your shop](https://coldtill.com/docs/guides/accept-payments.md).

- **Base URL:** your site URL, for example `https://pay.example.com`.
- **Authentication:** an `API-Key` header with a project API key. Create keys under **API keys** (project
  admin, with two-factor). Keys are shown once and stored hashed.
- **Format:** JSON request and response bodies. Amounts are decimal strings.
- **Errors:** a non-2xx status with `{"error": "message"}` and sometimes a `code`.
- **Rate limit:** 600 requests per minute per client IP. Exceeding it returns `429`.

An API key belongs to one project. It can create payments and payouts, but payouts can only go to that
project's address book, and large ones wait for an admin. See [Payouts](https://coldtill.com/docs/payouts.md).

## Payments

### Create a payment

`POST /api/v1/payment`

```bash
curl -X POST https://pay.example.com/api/v1/payment \
  -H "API-Key: $COLDTILL_API_KEY" -H 'Content-Type: application/json' \
  -d '{"customerID":"42","customerEmail":"jane@example.com","amountInUSD":25.00,"invoiceID":"INV-1001"}'
```

| Field | Required | Description |
|---|---|---|
| `customerID` | yes | Your ID for the customer. The same ID always gets the same deposit addresses |
| `amountInUSD` | yes | Amount owed, greater than 0 and at most 10,000,000 |
| `customerEmail` | no | Stored with the customer |
| `invoiceID` | no | Your order reference, echoed in webhooks |
| `network` | no | Restrict the checkout to one network code (see [Network codes](#network-codes)) |
| `currency` | no | With `network`, restrict to one currency, for example `USDC` |
| `expire` | no | RFC 3339 expiry time, at least a minute ahead. Defaults to the project's payment expiry (24 hours) |

Response:

```json
{
  "host": "https://pay.example.com",
  "reference_id": "fd09c7e5-d661-4355-9b04-7b3749b41e2e",
  "url": "https://pay.example.com/pay/fd09c7e5-d661-4355-9b04-7b3749b41e2e"
}
```

Send the customer to `url`. A customer has at most one open payment: creating a new one replaces the previous
one (`REPLACED`). If the previous payment has a deposit that is still confirming, the request fails with `409`
so that deposit isn't stranded; retry after it confirms.

### Get a payment

`GET /api/v1/payment/reference/{reference_id}`

```json
{
  "invoiceID": "INV-1001",
  "customerID": "42",
  "amountInUSD": "25",
  "filledAmountInUSD": "25.00000000",
  "pendingAmountInUSD": "0",
  "paymentState": "FILLED",
  "referenceID": "fd09c7e5-…",
  "url": "https://pay.example.com/pay/fd09c7e5-…",
  "createdAt": "2026-10-10T15:58:23Z",
  "expiresAt": "2026-10-11T15:58:23Z",
  "deposits": [
    {
      "network": "BASE", "currency": "USDC", "amount": "25", "amountInUSD": "25",
      "transactionHash": "0x…", "status": "confirmed", "confirmations": 12, "requiredConfirmations": 10
    }
  ]
}
```

Payment states:

| State | Meaning | Final |
|---|---|---|
| `OPEN` | Nothing confirmed yet | no |
| `PARTIALLY_FILLED` | Confirmed amount is below 99% of the amount owed | no |
| `FILLED` | Confirmed amount is within 1% of the amount owed | yes |
| `OVER_FILLED` | Confirmed amount is more than 1% above the amount owed | yes |
| `CANCELLED` | Expired before being filled | yes |
| `REPLACED` | Superseded by a newer payment for the same customer | yes |

Ship the order on `FILLED` or `OVER_FILLED`. Prefer webhooks to polling.

### Payable currencies

`GET /api/v1/blockchain-currency/reference/{reference_id}` lists what the customer can pay with for this
payment, with the price used for each.

### Deposit address

`POST /api/v1/deposit-address/reference/{reference_id}` with `{"blockchain_code": "BASE"}` returns the
customer's permanent address for that network's family. Use it to build your own checkout instead of the
hosted one.

### Ticker

`GET /api/v1/ticker` (no API key) lists enabled networks and tokens with USD prices:
`blockchainCode`, `currencyCode`, `tokenAddress`, `standard`, `walletPrecision`, `family`, `price`.

## Payouts

### Create a payout

`POST /api/v1/withdrawal/merchant` with an `Idempotency-Key` header:

```bash
curl -X POST https://pay.example.com/api/v1/withdrawal/merchant \
  -H "API-Key: $COLDTILL_API_KEY" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: refund-INV-1001' \
  -d '{"blockchainCode":"BASE","currencyCode":"USDC","amount":"12.50","toAddress":"0xAbC…"}'
```

| Field | Description |
|---|---|
| `blockchainCode` | Network code: an EVM network or `SOL` |
| `currencyCode` | `ETH`, `USDC`, `USDT`, `SOL`… as enabled on that network |
| `amount` | Decimal amount in the currency, at most the token's decimals |
| `toAddress` or `addressBookId` | Destination. Through the API it must be in the project's address book |

Retrying with the same `Idempotency-Key` and the same parameters returns the original payout. Reusing a key
with different parameters returns `409`.

The response is the payout (see [Payout object](#payout-object)) with status `pending` or `pending-approval`.

### List and get payouts

- `GET /api/v1/withdrawal/merchant?limit=50&offset=0` lists payouts created through the API.
- `GET /api/v1/withdrawal/{id}/merchant` returns one payout.

### Payout object

```json
{
  "id": "5d1c…", "status": "sent", "network": "BASE", "currency": "USDC",
  "tokenContract": "0x8335…", "amount": "12.5", "amountInUSD": "12.50", "toAddress": "0xAbC…",
  "transactionHash": "0x…", "blockNumber": 21234567, "idempotencyKey": "refund-INV-1001",
  "createdBy": "system", "createdAt": "…", "updatedAt": "…"
}
```

`approvalReason` explains why a payout is held; `failureReason` explains a failure. Statuses are listed in
[Payouts](https://coldtill.com/docs/payouts.md#statuses).

## Network codes

| Code | Mainnet | Testnet mode |
|---|---|---|
| `ETH` | Ethereum | Sepolia |
| `BASE` | Base | Base Sepolia |
| `POLYGON` | Polygon | Amoy |
| `BSC` | BNB Smart Chain | BSC testnet |
| `TRON` | Tron | Nile |
| `SOL` | Solana | Devnet |
| `BTC` | Bitcoin | Signet |

## SDK

`sdk/typescript` wraps these endpoints and verifies webhooks, with no dependencies:

```ts
import { Coldtill, verifyWebhookRequest, isPaid } from '@coldtill/sdk'

const kh = new Coldtill({ baseUrl: 'https://pay.example.com', apiKey: process.env.COLDTILL_API_KEY! })
const { url } = await kh.createPayment({ customerID: '42', amountInUSD: 25, invoiceID: 'INV-1001' })
```

The WooCommerce plugin adds Coldtill as a payment method for USD stores.

---

Source: https://coldtill.com/docs/api/ · All Coldtill docs: https://coldtill.com/llms.txt
