Payment API
Send payments to Statsy from your own server when your billing does not come from a supported provider webhook.
If you bill through something Statsy has no webhook for — your own billing code, an invoicing tool, a marketplace, a bank transfer you reconcile nightly — send the payment yourself. It lands in exactly the same place as a webhook payment.
The endpoint
POST https://statsy.co/api/v1/sites/<site>/payments<site> is the site's public id or its ws_ key. Authenticate with a bearer token created under Site settings › API tokens: a site token (st_) limited to one site is the right choice here. Account tokens (sta_) work too.
curl https://statsy.co/api/v1/sites/ws_XXXXXXXX/payments \
-H "Authorization: Bearer st_XXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"external_id": "inv_20260915_881",
"amount": 49.00,
"currency": "USD",
"kind": "one_time",
"email": "buyer@example.com",
"occurred_at": "2026-09-15T10:04:00Z"
}'Body fields
| Field | Required | Notes |
|---|---|---|
external_id | Yes | Your own unique id for this payment, up to 255 characters. This is what makes the call idempotent. |
amount | Yes | In major units — 49.00 means forty-nine dollars, not forty-nine cents. |
currency | Yes | A three-letter code. Lower case is accepted and upper-cased for you. |
kind | No | one_time (the default), renewal or refund. A refund is stored as a negative amount whichever sign you send. |
visitor_id | No | From statsy.visitorId(). The most reliable way to get the sale credited. |
email | No | Matched against identities you linked with statsy.identify(), and against known customers. |
customer_id | No | Your id for the customer. Groups payments into one customer record and a lifetime value. |
subscription_id | No | Ties renewals together and lets a later cancellation mark the customer churned. |
occurred_at | No | ISO timestamp or epoch. Defaults to now. Use the real time, or attribution looks at the wrong touches. |
provider | No | A label for where it came from. Defaults to api. |
raw | No | Any JSON object you want kept alongside the payment. |
Idempotency
A payment is unique per site and external_id. Send the same one twice and the second call changes nothing and reports duplicate: true; the first insert answers 201, a repeat answers 200. That makes the endpoint safe to retry from a job queue, and safe to replay over a backfill.
The response carries the externalId, whether it was a duplicate, the visitor Statsy matched it to (or null), and the USD amount it was converted to.
Reading payments back
GET /api/v1/sites/<site>/payments lists them, newest first, with from, to, limit (up to 200) and a cursor for paging. GET /api/v1/sites/<site>/customers returns customers with their lifetime value, first touch and status.
Tip. One-off payments do not need code at all. The Revenue page has a form: Record payments by hand.
Last updated · Markdown version