Skip to main content
Pro Plan10 minutesIntermediate

How do I integrate Paddle for revenue tracking?

Connect Zenovay to your Paddle Billing (v2) account to attribute revenue back to traffic sources, campaigns, and individual visitors. Sandbox-first setup with two credentials and webhook signature verification.

paddlerevenueattributionintegrationwebhooks
Last updated:

Paddle is the merchant-of-record payment provider used by many EU SaaS companies — Paddle handles EU VAT, US sales tax, refunds, chargebacks, and global compliance on your behalf, then sends you the net.

Connecting Paddle to Zenovay lets you see Paddle revenue alongside your traffic data — which campaigns close the deal, which pages convert, which sources have the best LTV.

You only need ONE thing: a Paddle API key with Write permission. Zenovay creates the webhook destination on your behalf, captures the signing secret via Paddle's API, and stores both — encrypted. Same flow whether you're connecting Sandbox or Live (auto-detected from the key prefix).

Setup (2 minutes)

  1. Generate an API key with Write permission in Paddle

    Click New API key → name it "Zenovay analytics" → permission: WriteCreate.

    Paddle shows the key ONCE. Copy it now. Live keys start with pdl_live_apikey_…; sandbox keys start with pdl_sdbx_apikey_….

  2. Paste it into Zenovay and click Connect

    1. In app.zenovay.com, go to Settings → Revenue Attribution.
    2. Click the Paddle card.
    3. Paste the API key. Zenovay auto-detects Sandbox vs Live from the prefix (shown below the input).
    4. Click Connect.

That's it. Behind the scenes, Zenovay:

  1. Validates the key against Paddle's /event-types endpoint.
  2. Calls POST /notification-settings to create a webhook destination at https://api.zenovay.com/api/webhooks/paddle/<your-website-id> subscribed to the four canonical events (transaction.completed, subscription.created, subscription.updated, subscription.canceled).
  3. Captures endpoint_secret_key from Paddle's response and stores it encrypted.
  4. Flips the card to Connected.

You never touch Paddle's dashboard. You never see or copy a signing secret. Every webhook from Paddle is verified using the stored secret before any database write.

"Where do I find my signing secret in Paddle?" — short answer: you don't need to

This is the most common confusion. Paddle deliberately hides the signing secret in its dashboard after the destination is created. There's no "reveal" button, no "show" link — only Rotate Secret (which invalidates the old one and gives you a new one). For accounts that DO show the secret at creation time, you have a brief window to copy it before it disappears.

This is normal Paddle behaviour, not a bug. It exists for a security reason: secrets in UIs leak to screenshots, support tickets, and shoulder-surfers.

That's exactly why Zenovay's Connect flow doesn't require you to find it. We use Paddle's API (which DOES expose the secret programmatically for your own destinations) to fetch it automatically. You never touch it.

What if I already created the destination manually?

That's fine. When you click Connect in Zenovay:

  • If a destination already exists at our URL, Zenovay reuses it and grabs the secret via API.
  • If it's currently inactive (e.g. someone deactivated it in Paddle's UI), Zenovay reactivates it.
  • No duplicate destinations, no manual cleanup.

What if I want to set it up manually anyway?

You can, but it's friction without benefit. If you insist:

Manual setup steps (not recommended)
  1. In Paddle → Notifications → DestinationsNew destinationWebhook.
  2. URL: https://api.zenovay.com/api/webhooks/paddle/YOUR_WEBSITE_ID (your website ID is the UUID in Settings → Websites in app.zenovay.com).
  3. Subscribe to All events (Zenovay silently ignores anything it doesn't handle).
  4. Save the destination.
  5. Here's where most people get stuck: Paddle may or may not show the signing secret on the next screen. If it doesn't:
    • Option A: Rotate the secret (Paddle → Destinations → click your destination → Rotate Secret). Copy the new value.
    • Option B: Delete this destination and use Zenovay's Connect flow instead (Zenovay will recreate it and grab the secret via API).
  6. In Zenovay → Paddle card → open the Advanced (optional) section after pasting your API key → paste the pdl_ntfset_… signing secret into the Webhook Secret field.
  7. Save.

For 99% of users, just use the Connect flow above and skip this.

For payment-to-traffic-source attribution to work most accurately, set the visitor's anonymous Zenovay ID as custom_data when creating the Paddle transaction or checkout on your server:

// Node.js example using Paddle's API directly
const visitorId = req.cookies['zv_visitor_id']; // however your client passes it

await fetch('https://api.paddle.com/transactions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PADDLE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: [{ price_id: 'pri_01h…', quantity: 1 }],
    customer_id: paddleCustomerId,
    custom_data: { zenovay_visitor_id: visitorId },
  }),
});

When the transaction completes, Zenovay reads data.custom_data.zenovay_visitor_id and joins the payment to the visitor's session — including the source, campaign, and pages they visited before paying.

If you don't set custom_data, attribution still works via the customer's email address (when present on the event), but it's less precise — only matches if the visitor previously identified themselves with the same email.

What attribution model is used

Zenovay supports two attribution windows:

  • First-touch — credit goes to the source the visitor arrived from on their first session.
  • Last-touch — credit goes to the source they arrived from on the session containing the payment.

Both are computed and shown side-by-side in the Revenue tab.

Step 5: Sandbox testing

The right way to verify your Sandbox integration is to do a real test checkout:

  1. In Paddle's sandbox dashboard, create a test product + price.
  2. Use a checkout overlay or hosted checkout URL on a test page that loads your Zenovay tracker.
  3. Complete the checkout with one of Paddle's test card numbers.
  4. Within ~10 seconds: Paddle sends a real, signed transaction.completed event to Zenovay. Verify it arrived by:
    • Loading the Revenue tab in Zenovay — the capture should appear with the buyer's email + amount.
    • Or running this SQL in your Supabase project:
      SELECT customer_email, amount, currency, customer_name, provider_customer_id
      FROM payment_events
      WHERE payment_provider='paddle'
      ORDER BY created_at DESC
      LIMIT 5;
      

If the capture doesn't appear after ~30 seconds:

  • Check the Verification status badge on the Paddle card in Zenovay's settings.
  • Confirm Sandbox vs Live mode matches on both sides. A Sandbox-environment Paddle dashboard fires events from sandbox-api.paddle.com; if Zenovay is set to Live mode the signature check will fail.
  • Confirm the notification destination is still enabled in Paddle (Paddle auto-disables destinations after 5 consecutive 5xx responses; if Zenovay was deploying during your test, re-enable it).

Currency support

Paddle returns transaction amounts in the smallest unit of the currency (for example, "4999" for $49.99 USD). Zenovay converts this correctly per ISO 4217:

  • Decimal currencies (USD, EUR, GBP, …): divide by 100 → 4999 becomes 49.99.
  • Zero-decimal currencies (JPY, KRW, VND, …): use the value as-is → 5000 JPY stays 5000.
  • Three-decimal currencies (BHD, KWD, OMR, …): divide by 1000 → 1500 BHD becomes 1.500.

You don't need to configure anything — Zenovay does this automatically based on currency_code on the event.

Switching from Sandbox to Live

When you're ready for production:

  1. In Paddle's Live dashboard (vendors.paddle.com, not sandbox-vendors), repeat Step 1 and Step 2 to create a Live notification destination and a Live API key.
  2. In Zenovay, click the Paddle card → Disconnect (keeping history is fine) → click again to reconnect → paste the Live credentials → switch the toggle to Live → Save.

The same Zenovay webhook URL works for both environments; Paddle sends the events to whichever destination matches your active API key.

Disconnecting

Settings → Revenue Attribution → click the Paddle card → Disconnect.

By default, disconnecting just removes your Paddle credentials. Your existing payment records, attribution history, and Revenue dashboard data stay intact (subject to your plan's data retention window). You can reconnect at any time and continue from where you left off.

Optional: also delete historical Paddle data

The disconnect dialog includes a checkbox: "Also permanently delete N Paddle records ($X total)". Tick it only if you want a clean slate — for example, decommissioning a test integration or doing privacy housekeeping.

When checked, Zenovay will:

  • Delete all Paddle payment_events rows for this website
  • Delete all Paddle payments rows for this website
  • Clear the paddle_customer_id field on every identified user (the user record itself is preserved — they keep their Stripe ID, etc.)

The deletion runs in a single transaction — if any step fails, all three are rolled back, so you never end up in a half-deleted state. The action is irreversible.

Don't forget to disable the Paddle-side notification destination

Disconnecting in Zenovay only stops Zenovay from accepting events. Paddle will keep firing the destination at our endpoint until you disable it in your Paddle dashboard:

  1. Open Paddle → Notifications → Destinations.
  2. Find the destination pointing at https://api.zenovay.com/api/webhooks/paddle/{your-website-id}.
  3. Click it → toggle Disabled (or Delete).

Until you do, our endpoint responds 400 "Webhook not configured for this website" to each delivery. Paddle will eventually auto-disable the destination after enough failures — but explicit removal is cleaner and prevents the destination from cluttering your dashboard.

Limitations (V1)

  • Refund events are not yet rendered. Subscribe to adjustment.created / adjustment.updated in your destination anyway — Zenovay silently ignores them today and will start rendering them as soon as V2 ships.
  • subscription.past_due and subscription.paused events are received and dedup-recorded, but not yet rendered in the dashboard. Subscription status flips to past_due correctly, but you don't get a dedicated incident view.
  • One Paddle account per Zenovay website. If you accept payments through multiple Paddle accounts (e.g. one per geo subsidiary), configure each on its own Zenovay website.
  • OAuth-redirect connect is not supported. Manual credential entry only.

Troubleshooting

"Invalid Paddle API key" on Save

Three usual suspects:

  1. Wrong environment. A pdl_sdbx_apikey_… key with the Live toggle on (or vice versa) fails validation. Zenovay surfaces this with a specific error message — match the toggle to the prefix.
  2. Key must have Write permission AND be unrevoked. Read-only keys pass validation but fail when Zenovay tries to create the destination (403). Revoked keys fail validation outright (401). Generate a fresh Write-scoped key.
  3. The key was for a different team / account. Paddle API keys are scoped to a single Paddle account; pasting a key from a different account fails immediately.

"Webhook not configured for this website"

Either you haven't saved Paddle credentials in Zenovay yet, or you're testing the wrong webhook URL. The URL must include the exact YOUR_WEBSITE_ID from app.zenovay.com → Settings → Websites.

"Signature verification failed: replay_window_exceeded"

Zenovay rejects webhook events with timestamps more than 5 minutes off from the current time. This usually means clock skew on your side or Paddle's side. If you see this on a fresh test:

  • Restart your server (clock drift is uncommon but happens after long uptime).
  • Re-trigger the webhook from Paddle's dashboard (Notifications → Deliveries → Resend).

Verified webhook arrives but the transaction is missing from the Revenue tab

Paddle webhook arrives in 3 distinct shapes depending on whether the customer was created standalone or as part of a checkout. Zenovay's parser is defensive but if you see a verified delivery in your logs and nothing in Revenue:

  • Check payment_events directly:
    SELECT * FROM payment_events
    WHERE payment_provider='paddle'
    ORDER BY created_at DESC LIMIT 5;
    
  • If customer_email is NULL on the row, attribution fell back to paddle_customer_id matching. The transaction is recorded but won't show up in identified-user views until the same customer is linked via a future identified-user upsert.

Was this article helpful?