# Payments

> Attribute Stripe and Paddle revenue to sessions, campaigns, and identified users with Databuddy webhooks.


import { Callout, CodeBlock, Tab, Tabs } from "@/components/docs";
import { STRIPE_WEBHOOK_EVENTS } from "@databuddy/shared/stripe-webhooks";

Connect your payment provider and every transaction shows up in the Revenue dashboard, attributed to the session, referrer, and user that produced it.

> **TL;DR:** Generate a webhook URL in the dashboard, add it to Stripe or Paddle, and pass Databuddy IDs in your payment metadata.

---

## 1. Generate your webhook URL

1. Open your [Databuddy dashboard](https://app.databuddy.cc) and go to **Website → Revenue**.
2. Click **Generate webhook URLs**. You get one URL per provider, each with a unique secret path.
3. Keep this page open, you'll paste the URL into your provider next.

## 2. Add the webhook to your provider

<Tabs items={['Stripe', 'Paddle']}>
<Tab value="Stripe">

In the [Stripe dashboard](https://dashboard.stripe.com/webhooks), add an endpoint with your generated URL and subscribe to these events:

<table>
<thead><tr><th>Event</th><th>Purpose</th></tr></thead>
<tbody>
{STRIPE_WEBHOOK_EVENTS.required.map(({ event, purpose }) => (
  <tr key={event}><td><code>{event}</code></td><td>{purpose} ({event === "invoice_payment.paid" ? "required on API 2025-05-28.basil or later" : "required"})</td></tr>
))}
</tbody>
</table>

Databuddy expects Stripe API version `2025-05-28.basil` or later, because `invoice_payment.paid` was introduced in that version and is the canonical money event for anything billed through an invoice. Keep `payment_intent.succeeded` enabled for one-time payments.

`invoice.paid` and `invoice_payment.paid` carry different halves of the same fact, and you need both. The payment event carries the amount but references its invoice by ID only, so it cannot see your metadata. The invoice event carries the metadata but no payment amount, because Stripe does not include the invoice's `payments` list in webhook deliveries. Databuddy records the amount from the payment event and joins the metadata from the invoice event, which is why a subscription payment stays unattributed if only one of the two is enabled. Order does not matter, and redelivering either event is safe.

Paste the endpoint's **signing secret** into the Revenue settings so Databuddy can verify each delivery.

</Tab>
<Tab value="Paddle">

In the [Paddle dashboard](https://vendors.paddle.com), create a notification destination with your generated URL and subscribe to `transaction.completed`.

Paste the **webhook secret key** into the Revenue settings so Databuddy can verify each delivery.

</Tab>
</Tabs>

## 3. Pass Databuddy IDs with each payment

Metadata connects a payment to the visitor who made it. Include what you have: every field is optional, and more fields mean stronger attribution.

<Tabs items={['Stripe', 'Paddle']}>
<Tab value="Stripe">
<CodeBlock language="ts">
  {`import { getProfileId, getTrackingIds } from "@databuddy/sdk";

// In the browser, collect the visitor's IDs...
const { anonId, sessionId } = getTrackingIds();
const profileId = getProfileId();

// ...send them to your server and reuse the same metadata:
const databuddyMetadata = {
  databuddy_client_id: websiteId,
  databuddy_session_id: sessionId,
  databuddy_anonymous_id: anonId,
  databuddy_profile_id: profileId,
};

await stripe.paymentIntents.create({
  amount,
  currency,
  metadata: databuddyMetadata,
});

// For recurring billing, attach it to the Subscription too.
await stripe.subscriptions.create({
  customer,
  items: [{ price: priceId }],
  metadata: databuddyMetadata,
});`}
</CodeBlock>

Stripe does not automatically copy PaymentIntent metadata to future subscription invoices. Put the IDs on the Subscription (or `subscription_data.metadata` when using Checkout) so recurring revenue stays attributable.
</Tab>
<Tab value="Paddle">
<CodeBlock language="ts">
  {`import { getProfileId, getTrackingIds } from "@databuddy/sdk";

const { anonId, sessionId } = getTrackingIds();

Paddle.Checkout.open({
  items,
  customData: {
    website_id: websiteId,
    session_id: sessionId,
    anonymous_id: anonId,
    profile_id: getProfileId(),
  },
});`}
</CodeBlock>
</Tab>
</Tabs>

| Field | What it unlocks |
|---|---|
| Session ID | Ties revenue to the exact visit, referrer, and campaign |
| Anonymous ID | Ties revenue to the device even without a session |
| Profile ID | Ties revenue to the identified user, see [Identify Users](/docs/sdk/identify-users) |

<Callout type="info">
Anonymous IDs are hashed on arrival, the same way the tracker hashes them; raw
device IDs are never stored. Profile IDs are your own user IDs and are stored
as sent.
</Callout>

<Callout type="warn">
Only the browser tracker creates the sessions that revenue attribution joins
against. Server-side `track()` calls through the Node SDK or the `/track` API
are stored as custom events, so a session ID you mint on the server has no
referrer, campaign, country, or device behind it and will resolve as
unattributed. Take `sessionId` from `getTrackingIds()` in the browser and pass
it to your server, rather than generating one server-side.
</Callout>

## Verify it works

Send a test payment (Stripe test mode works), then check **Website → Revenue**. The transaction appears within seconds of the webhook delivery. If you passed a profile ID, the revenue also shows on that user in **Website → Users**.

## Troubleshooting

- **Nothing appears:** confirm the webhook URL matches the generated one exactly and the signing secret is saved in Revenue settings; deliveries with bad signatures are rejected.
- **Revenue appears but isn't attributed:** the payment had no Databuddy metadata. Attribution only works for payments created after you started passing IDs.
- **One payment type never attributes:** check that your endpoint subscribes to every required event. Stripe subscribes an endpoint to a fixed event list, and adding a new payment flow does not update it. Revenue settings shows when Databuddy last recorded each event, which helps you spot a type that stopped arriving, but an event that simply has not happened yet (a refund, a failed payment) also shows no activity, so compare it against the event list in your Stripe dashboard rather than treating it as proof.
- **A session ID you sent isn't recognised:** it has to come from the browser tracker. IDs generated on the server, or sent through server-side `track()`, never produce a joinable session.
- **Stripe shows failed deliveries:** regenerating webhook URLs invalidates the old path, so update Stripe after regenerating.
