Skip to content

Payment API

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

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"
FieldRequiredNotes
external_idYesYour own unique id for the payment. Doubles as the idempotency key.
amountYesA number in whole currency units. For kind: refund it is stored as a negative.
currencyYesA three-letter code, for example USD or INR.
kindNoone_time (the default), renewal or refund.
visitor_idNoThe visitor to credit. From statsy.visitorId() in the browser.
emailNoUsed to match a visitor identified earlier with statsy.identify().
customer_idNoYour customer id, for lifetime value and repeat purchases.
subscription_idNoGroups renewals of the same subscription.
occurred_atNoISO timestamp or epoch. Defaults to now.
providerNoA label for where it came from. Defaults to api.
rawNoAny 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.

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.

Last updated · Markdown version