Billing webhooks

Last updated Aug 12, 2026View as MarkdownAgent setup

MRR and revenue with zero product code: paste your webhook signing secret in the dashboard, point the biller at your endpoint, done.

ProviderEndpoint
Stripehttps://in.datagauge.dev/v1/webhooks/stripe/{project_id}
Polarhttps://in.datagauge.dev/v1/webhooks/polar/{project_id}

{project_id} is the app's proj_… project id, shown on the app's page in the dashboard and returned as public_id by GET /v1/apps. It is the same value metric reads call project. It is not the app's app_id, which is a different string in the same JSON object; a k17b2c3… in this path is a 404.

Signatures are verified with your per-project secret (Stripe Stripe-Signature, Polar Standard-Webhooks), with a 5-minute replay window. Secrets are stored server-side and never shown again. You can set them headlessly too, with the Control API: PUT /v1/apps/{id}/webhooks/{provider}.

Rehearsing against your biller's test mode

Each plane has its own endpoint and its own signing secret. Point your biller's test webhook at your app's test_public_id, and paste the secret from that same test endpoint:

PlaneEndpointSecret set by
live…/v1/webhooks/stripe/{public_id}the dashboard in live mode, or a dg_live_ token
test…/v1/webhooks/stripe/{test_public_id}the dashboard in test mode, or a dg_test_ token

The two never meet. A test secret cannot verify a live delivery, and a live one cannot verify a test delivery, because they are separate entries addressed by separate project ids. So a test subscription's invoice.paid lands in your test metrics and moves nothing you would put in front of an investor.

The plane comes from the credential, never from the request: a dg_test_ token writes the test secret and has no way to address the live one. If you send environment in the body it must match your token's plane, or the write is a 400. It is an assertion, not a switch.

A brand-new app has no test project until something asks for one, so test_public_id is null and the endpoint URL does not exist yet. Saving a test signing secret creates it, in the dashboard and over the Control API alike. No key is minted in the process.

Two Stripe endpoints, two secrets

Stripe issues a different whsec_… for every endpoint you create, including the one in test mode. Copying the live secret into the test plane produces an endpoint that verifies nothing: every delivery fails its signature check, and Stripe disables the endpoint after retrying for days.

What gets mapped

  • Stripe: invoice.paid → a payment; customer.subscription.* → the subscription object, MRR monthly-normalized from the line items (annual ÷ 12, quantity-aware).
  • Polar: order.paid → a payment; subscription.* → the subscription object.

Event types we don't consume are acknowledged with 200, so your biller never retry-spams.

Retries can't double-count

Billers deliver at-least-once. Every mapped payment carries a stable id derived from the biller's object (stripe:in_123), and the money store dedupes on it structurally. Send the same invoice.paid five times: revenue moves once.

Oversized fields are shortened, never dropped

Billers put customer-controlled strings in their objects, and nothing bounds how long they get. If a field arrives too long to store, we shorten it, mark it with #truncated#, and keep the payment. The response says which fields were touched:

{ "ok": true, "rows": 1, "truncated": ["payments.user"] }

Refusing the delivery instead would only make your biller retry the same bytes until it gave up and disabled the endpoint, and you would be missing the money, not just the id.

A shortened value keeps a fingerprint of the whole original (its length and a 64-bit hash), so two different oversized ids stay two different payments unless they match on both. That is not the same as "never": it is a fingerprint, not the value. Nothing a biller sends can reach it, since Stripe and Polar ids are far shorter than the limit and are never truncated at all.

While your own subscription is not in good standing

If your DataGauge account is suspended, either for an unpaid invoice or because your subscription has ended, deliveries to these endpoints are verified and acknowledged, but not stored:

{ "ok": true, "rows": 0, "suspended": true, "code": "ingest_suspended" }

Still a 200, deliberately. A failure status would not reach you; it would make Stripe or Polar retry the delivery for days and then disable your endpoint, which is a worse thing to be left with than a gap. Nothing already stored is deleted, reads keep working, and storage resumes on the next delivery once the invoice is paid or the subscription is restarted.

Marketplaces / Stripe Connect

An invoice carrying an application_fee_amount is split automatically: the fee is your revenue (revenue metric); the gross is volume processed for someone else (volume metric). Other people's money never inflates your portfolio.