# Payment API

> Send payments from any system so they count towards revenue attribution, and read back payments and customers.

Section: CLI, MCP and API  
Canonical page: https://statsy.co/docs/developers/api-payments  
Last updated: 2026-09-15

Most revenue arrives through a signed webhook from Stripe, Lemon Squeezy, Paddle, Polar or Razorpay. The Payment API is for everything else: an in-house billing system, a marketplace, an invoice paid by bank transfer.

## Record a payment

`POST /sites/:site/payments` takes one payment in the same shape the webhooks produce. It needs owner or editor.

```bash
curl -X POST -H "Authorization: Bearer sta_your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "inv_10024",
    "amount": 49.00,
    "currency": "USD",
    "kind": "one_time",
    "visitor_id": "v_8f2c...",
    "email": "buyer@example.com",
    "occurred_at": "2026-09-15T10:00:00Z"
  }' \
  "https://statsy.co/api/v1/sites/ws_yoursitekey/payments"
```

| Field | Required | Notes |
| --- | --- | --- |
| `external_id` | Yes | Your own unique id for the payment. Doubles as the idempotency key. |
| `amount` | Yes | A number in whole currency units. For `kind: refund` it is stored as a negative. |
| `currency` | Yes | A three-letter code, for example USD or INR. |
| `kind` | No | `one_time` (the default), `renewal` or `refund`. |
| `visitor_id` | No | The visitor to credit. From `statsy.visitorId()` in the browser. |
| `email` | No | Used to match a visitor identified earlier with `statsy.identify()`. |
| `customer_id` | No | Your customer id, for lifetime value and repeat purchases. |
| `subscription_id` | No | Groups renewals of the same subscription. |
| `occurred_at` | No | ISO timestamp or epoch. Defaults to now. |
| `provider` | No | A label for where it came from. Defaults to `api`. |
| `raw` | No | Any object you want kept alongside the payment. |

## Sending the same payment twice

`external_id` is unique per site. A new payment returns `201`; sending the same `external_id` again returns `200` with `duplicate: true` and changes nothing. That makes retries safe, so always retry a timed-out request rather than generating a new id.

## Getting a payment attributed

A payment always counts towards revenue. Crediting it to a channel needs a link back to a visit, in this order: the `visitor_id` you send, an `email` the site previously linked to a visitor with `statsy.identify()`, or a known customer matched by `customer_id`. Send none of these and the payment shows as revenue but not under any source. See [Payments and revenue problems](https://statsy.co/docs/troubleshooting/payments).

> **Tip.** Capture `statsy.visitorId()` when the visitor starts checkout and carry it to your server with the order. Reading the cookie at payment time often fails, because the payment is confirmed by a server, not a browser.

## Read payments and customers

`GET /sites/:site/payments` lists payments newest first. It takes `from`, `to`, `limit` (100 by default, 200 at most) and `cursor`, and returns the next page under `nextCursor`. Each row carries the provider, external id, visitor, customer, email, amount, currency, USD amount, kind, timestamp and subscription id.

`GET /sites/:site/customers` lists customers by lifetime value, highest first, with `query` searching email, external id and visitor id. Each row carries lifetime value, status, when they first paid and their first touch.

Previous: [Goals and funnels API](https://statsy.co/docs/developers/api-goals-and-funnels)  
Next: [Alerts and share links API](https://statsy.co/docs/developers/api-alerts-and-share)