Payments and revenue
Payments that never arrive, payments credited to Direct, refunds, duplicates, and why browser-reported revenue is ignored.
Revenue reaches Statsy in one of two ways: a signed webhook from your payment provider, or the Payment API. Nothing about revenue is ever taken from the browser, so problems are almost always at the webhook or at the visitor match.
No payments arriving at all
Check the delivery log in your provider’s dashboard first: it shows the exact response Statsy sent back. That response names the problem.
| Response | What it means |
|---|---|
invalid_signature | The signing secret does not match. Copy it again from the provider and paste it into Site settings › Integrations. A test-mode secret will not verify live events. |
integration_missing | No integration of that kind is configured for this site. Add it before pointing the webhook here. |
site_not_found | The site key in the webhook URL is wrong. Copy the URL from the integrations screen again. |
unknown_provider | The provider segment of the URL is misspelt. |
invalid_json | The body was not JSON. Usually a proxy or a test tool rewriting the request. |
handled: false with a reason | The webhook arrived and verified, but that event type is not one we record. Check which events your endpoint subscribes to. |
Watch out. Stripe signatures are only accepted within five minutes of being sent. A badly wrong server clock, or replaying an old webhook by hand, fails verification even with the right secret.
Subscribe to the right events
Only some event types produce a payment. For Stripe those are checkout.session.completed, invoice.paid for renewals, charge.refunded and customer.subscription.deleted. Each provider has its own equivalents, listed on the integration screen. If your endpoint only subscribes to, say, payment_intent.succeeded, nothing will be recorded.
Test mode
Statsy does not distinguish test payments from real ones. If you paste a test-mode signing secret and point a test endpoint at the same URL, test payments are recorded as real revenue. Use a separate site in Statsy for testing, and remember to swap the secret to the live one when you go live.
Revenue shows up under Direct
This is the most common report, and it is not a missing payment: the payment was recorded, but nothing linked it back to a visit, so it falls back to the Direct channel. A payment is matched to a visitor by, in order, an identifier carried through checkout, an email the site previously linked with statsy.identify(), or a known customer id.
- Hosted checkout links get the identifier automatically when they point at Stripe Checkout, Lemon Squeezy, Polar, Paddle or Razorpay and the visitor is in Full mode. Your own checkout page, or a payment link on a custom domain, is not recognised.
- Cookieless visitors carry no identifier. Visitors in the EU, EEA, UK and Switzerland, and anyone sending Global Privacy Control, are counted without one by default. Their payments count as revenue but can only be credited by email or customer id.
- Server-created checkouts need the id passed explicitly. Capture
statsy.visitorId()when checkout starts and send it along, for example as Stripe’sclient_reference_idor asstatsy_vidin the provider’s metadata.
const vid = window.statsy?.visitorId?.();
await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId, statsy_vid: vid }),
});Revenue shows as zero
If you call statsy.revenue() in the browser and expect an amount, that is why. Browser-reported amounts are deliberately discarded: the call is kept only for compatibility and records a purchase conversion with no value. Real amounts come from webhooks and the Payment API, where they are signed or authenticated and cannot be forged from a page.
Duplicate or missing payments
Each payment is stored once per site under its external id, so replaying a webhook is safe and changes nothing. Duplicates therefore mean two genuinely different ids: usually two webhook endpoints pointing at the same site with different events, or a manual Payment API call for something a webhook already sent. Send the provider’s own id as external_id and the two will collapse into one.
Refunds appear as negative amounts and reduce the revenue of the channel that got the credit, which is why a channel can go down after a good week.
Last updated · Markdown version