# Guide: accept a payment from your shop

This walks through the whole flow once: create an API key, create a payment, send the customer to checkout, and
mark the order paid from a webhook. It takes about fifteen minutes on a server that has finished
**Get started** in the dashboard (a cold wallet and at least one deposit wallet).

The full reference is [`openapi.yaml`](https://coldtill.com/openapi.yaml), rendered at
[coldtill.com/docs/api](https://coldtill.com/docs/api).

## 1. Create an API key and a webhook

1. In the dashboard, open **API keys → New key**. Copy the key (`ct_…`); it is shown once. Store it as
   `COLDTILL_API_KEY` in your shop's environment, never in the browser.
2. Open **Webhooks → Add endpoint** with your shop's URL, for example `https://shop.example.com/coldtill/webhook`.
   Copy the signing secret as `COLDTILL_WEBHOOK_SECRET`.

## 2. Create a payment when the customer checks out

Use your own order number as `invoiceID` and a stable customer ID: the same customer always gets the same deposit
addresses.

```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"}'
```

```ts
// Node 18+, with the SDK: npm install @coldtill/sdk
import { Coldtill } from '@coldtill/sdk'

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

```php
$res = wp_remote_post('https://pay.example.com/api/v1/payment', [
    'headers' => ['API-Key' => getenv('COLDTILL_API_KEY'), 'Content-Type' => 'application/json'],
    'body'    => wp_json_encode(['customerID' => '42', 'amountInUSD' => 25.00, 'invoiceID' => 'INV-1001']),
    'timeout' => 20,
]);
$payment = json_decode(wp_remote_retrieve_body($res), true);
wp_redirect($payment['url']);
```

```python
import os, requests

r = requests.post(
    "https://pay.example.com/api/v1/payment",
    headers={"API-Key": os.environ["COLDTILL_API_KEY"]},
    json={"customerID": "42", "amountInUSD": "25.00", "invoiceID": "INV-1001"},
    timeout=20,
)
r.raise_for_status()
payment = r.json()  # {"host": ..., "reference_id": ..., "url": ...}
```

Save `reference_id` with the order, then redirect the customer to `url`. If the project has a
[checkout domain](https://coldtill.com/docs/configuration.md#checkout-domains), the link uses it.

A `409` means the customer's previous payment still has a deposit confirming; show "try again in a few minutes".

## 3. Mark the order paid from the webhook

Coldtill posts to your endpoint as deposits arrive and confirm. Verify the signature on the **raw** body, then act
on the signed `status`:

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

app.post('/coldtill/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const ev = await verifyWebhookRequest(req.body, req.headers, process.env.COLDTILL_WEBHOOK_SECRET!)
    if (ev.event.startsWith('payment.') && isPaid(ev.data.status)) {
      await markOrderPaid(ev.data.invoice_id, ev.data.reference_id) // must be idempotent
    }
    res.sendStatus(200)
  } catch {
    res.sendStatus(401)
  }
})
```

PHP, Go and Python versions are in [Webhooks](https://coldtill.com/docs/webhooks.md#verifying-requests).

Rules that avoid the usual mistakes:

- Ship on `FILLED` or `OVER_FILLED` only. `PARTIALLY_FILLED` means the customer sent too little; the checkout page
  asks them for the rest.
- Check that `reference_id` matches the one you saved for `invoice_id`, so an old payment can't mark a newer order.
- Make `markOrderPaid` idempotent: the final event can be retried for up to two days.
- Respond within 60 seconds. Do slow work (emails, fulfilment) after responding.

## 4. Handle the edges

| What happened | Webhook status | What to do |
|---|---|---|
| Customer never paid | `CANCELLED` after the expiry (24 h by default) | Cancel the order or offer a new link |
| Customer started again | `REPLACED` | Ignore; the newer payment carries on |
| Paid 10% too much | `OVER_FILLED` | Ship; refund the difference with a payout if you like |
| Paid late, after expiry | progress event with a new `filled_amount` | Decide by hand; the payment stays closed |

## 5. Test it

On a testnet server (`COLDTILL_MODE=testnet`), pay with Base Sepolia USDC or Sepolia ETH from a faucet, and point
the webhook at `http://localhost`. **Webhooks → Send test** sends a signed test event at any time. Then switch to a
mainnet server and pay yourself $1 before going live.

## Refunds and supplier payments

Payouts send from the hot wallet. Through the API they can only go to addresses in the project's address book, and
amounts over the project's limits wait for an admin. See [Payouts](https://coldtill.com/docs/payouts.md) and the `createPayout`
operation in the reference.

---

Source: https://coldtill.com/docs/guides/accept-payments/ · All Coldtill docs: https://coldtill.com/llms.txt
