# Payments and revenue

> Payments that never arrive, payments credited to Direct, refunds, duplicates, and why browser-reported revenue is ignored.

Section: Troubleshooting  
Canonical page: https://statsy.co/docs/troubleshooting/payments  
Last updated: 2026-09-15

Revenue reaches Statsy in one of two ways: a signed webhook from your payment provider, or the [Payment API](https://statsy.co/docs/developers/api-payments). 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’s `client_reference_id` or as `statsy_vid` in the provider’s metadata.

```js
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.

Previous: [Ad blockers and the proxy host](https://statsy.co/docs/troubleshooting/ad-blockers)  
Next: [Heatmap snapshots](https://statsy.co/docs/troubleshooting/heatmap-snapshots)