Skip to main content
Highseat records revenue from your payment provider’s webhook. The amount, the currency and the product names come from the provider, never from the browser, so a blocked tracker cannot lose a sale and an edited request cannot invent one. Each site connects its own providers in Site settings › Revenue, on the Payment providers card. Connecting takes three steps, which the Connect button on a provider’s row walks you through: add a webhook endpoint in the provider, select its events, and paste the secret the provider shows for that endpoint. The guides below follow the same steps in each provider’s own dashboard.

Stripe

1

Copy the webhook URL

In Highseat, open Site settings › Revenue and click Connect on the Stripe row. Copy the URL from the first step. It looks like https://app.highseat.com/api/webhooks/stripe/… and belongs to this site alone.
2

Add the endpoint in Stripe

In the Stripe Dashboard, open the Webhooks tab in Workbench (Developers › Webhooks, or dashboard.stripe.com/webhooks) and click Create an event destination.
  1. Select Your account.
  2. Choose the Snapshot payload format. Highseat reads the payment from the event itself, and a thin event carries only the object’s id.
  3. Select these six events: checkout.session.completed, checkout.session.async_payment_succeeded, invoice.paid, refund.created, charge.dispute.funds_withdrawn and charge.dispute.funds_reinstated.
  4. Click Continue, select Webhook endpoint, and click Continue again.
  5. Paste the URL from Highseat as the Endpoint URL and create the destination.
3

Paste the signing secret

On the endpoint’s page in Stripe, reveal the signing secret and copy it. It starts with whsec_. Paste it into the third step in Highseat and click Connect Stripe.
The row then reads “Connected. Waiting for the first event.” until Stripe sends one. Stripe keeps a sandbox and live mode apart, each with its own endpoints and signing secrets. Highseat records a sandbox sale the same way as a live one, so connect the live endpoint once a test sale has shown up.

What Stripe sends

  • A one-time Checkout payment is recorded when the session is paid. A payment method that settles later, such as a bank transfer or SEPA Direct Debit, is recorded on checkout.session.async_payment_succeeded, when the money arrives.
  • A subscription is recorded from invoice.paid, for the first payment and every renewal. Any other paid invoice is recorded the same way.
  • A refund comes off the sale it belongs to.
  • A dispute comes off the sale it belongs to when Stripe withdraws the disputed amount (charge.dispute.funds_withdrawn). When you win the dispute and Stripe returns the funds (charge.dispute.funds_reinstated), the amount goes back on.
A payment made outside Checkout and invoices, such as a PaymentIntent your own code confirms, is not recorded. Stripe amounts include tax. An endpoint that subscribes to only the first four events records no disputes. To record them on a site that is already connected, open its endpoint in the Stripe Dashboard and add charge.dispute.funds_withdrawn and charge.dispute.funds_reinstated to the events it sends. The URL and the signing secret stay the same.

Product names for Checkout

Stripe’s Checkout webhook carries no line items, so a one-time Checkout sale shows its product as “Unknown”. A restricted API key lets Highseat read the line items and name the product. Invoices carry their lines already and need no key.
  1. In the Stripe Dashboard, open the API keys page (dashboard.stripe.com/apikeys) and click Create restricted key.
  2. Name the key, set Checkout Sessions to Read, and leave every other permission at None.
  3. Click Create key and copy the key. It starts with rk_live_.
  4. In Highseat, click Add key under Restricted API key on the Stripe row, paste the key and click Save.
Highseat makes one test read with the key when you save it, and tells you when the key is rejected or lacks the permission.

Polar

1

Copy the webhook URL

In Highseat, open Site settings › Revenue and click Connect on the Polar row. Copy the URL from the first step. It looks like https://app.highseat.com/api/webhooks/polar/… and belongs to this site alone.
2

Add the endpoint in Polar

In Polar, open your organization’s Settings › Webhooks and click Add Endpoint.
  1. Paste the URL from Highseat into the URL field.
  2. Leave the format on Raw.
  3. Generate a secret, or set your own, and copy it.
  4. Select these two events: order.paid and refund.created.
  5. Create the endpoint.
3

Paste the webhook secret

Paste the secret into the third step in Highseat and click Connect Polar.
Polar is the merchant of record, so Highseat records each order’s net amount: after discounts, before tax. A one-time order and every subscription payment arrive as order.paid, and a refund comes off the order it belongs to. Polar sends no webhook event for a dispute, so Highseat does not see a chargeback on a Polar order. Polar’s sandbox is a separate organization with its own webhooks, and Highseat records its orders like live ones.

Pass the attribution id to your checkout

A webhook says what was sold. It cannot say which visit sold it, because the provider never sees your pages. The tracker gives each browser tab an attribution id, and your checkout carries it to the provider in the checkout’s metadata, under the key attribution_id. The sale then joins the visit that opened the checkout. Read the id in the browser with getAttributionId() from the highseat package:
getAttributionId() never makes a checkout hang. It resolves to undefined at once when the page has no tracker script or the script failed to load, and after 3 seconds when the script is still loading. Pass { timeout } in milliseconds to wait less, for example getAttributionId({ timeout: 500 }) on a page where the tracker has long loaded. On a site with the plain script tag, window.highseat.getAttributionId() returns the id once the tracker has loaded. Where the id goes depends on the checkout:

Create the checkout on your server

A provider checkout is created on your server, with your secret key, so the id travels in two hops: the browser reads it and sends it with the checkout request, and the server writes it into the metadata.
Browser
The server side, for each provider:
app/api/checkout/route.ts
For a subscription, set mode: "subscription" and pass the same object as subscription_data: { metadata }. Stripe Checkout runs on Stripe’s own page in the buyer’s browser, so Stripe reads the buyer’s IP address itself.

How a sale finds its visit

The attribution id reaches the events of the tab that opened the checkout. Their sessions bring in every page view of the same visit, so a checkout opened in a new tab still credits the tab that arrived from a search engine. When your site calls identify() for a signed-in buyer, every earlier session of that user within the lookback window joins too. A buyer who signed up from a Reddit post two weeks before paying in a direct visit then credits Reddit under first touch. Only a user id seen before the payment counts, so a later sign-in never changes a sale’s credit. The Attribution card in Site settings › Revenue picks the Model (Last touch by default, or First touch, or Linear) and the Lookback (30 days by default, or 7, 14 or 90). A change applies to every past sale.

A provider sale and a tracker purchase

With a provider connected, its webhook is the authoritative record of a sale. A purchase event from track() whose order_id matches the provider’s order id is left out of revenue totals, so connecting a provider never counts a sale twice. The order id is: Send purchase without revenue for a checkout a provider reports. Send revenue only for a checkout no provider reports, and then with an order_id. A tracker purchase with a matching order_id still lends the sale its session when the checkout passed no attribution id.

When a sale shows no source

getAttributionId() resolves to undefined when an ad blocker or a privacy browser blocks the tracker, or when the tracker has not loaded within the timeout. The checkout still opens and the sale is still recorded, with no visit to credit. It counts in your revenue totals but appears under no source, since a source is read from the page view that started a visit. A paid order with no attribution id and no tracker events from the buyer is this case, not a broken integration.

Status of a connected provider

Each provider’s row on the Payment providers card shows one line: A failing row stays connected. The provider retries each delivery, and the next event that verifies turns the row healthy again.

Fix a failing signature

A signature fails when the secret Highseat holds is not the one the provider signs with. Either the secret was rotated in the provider, or the pasted secret belongs to another endpoint, such as the sandbox endpoint or another site’s. Copy the current secret from the endpoint whose URL matches the one on the row, click Replace next to the secret in Highseat, paste it and save. The provider’s next retry then verifies. To stop receiving sales from a provider, click Disconnect… on its row, and delete the endpoint in the provider too, or the provider keeps retrying.