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)
Generate an API key with Write permission in Paddle
Click New API key → name it "Zenovay analytics" → permission: Write → Create.
Paddle shows the key ONCE. Copy it now. Live keys start with
pdl_live_apikey_…; sandbox keys start withpdl_sdbx_apikey_….Paste it into Zenovay and click Connect
- In
app.zenovay.com, go to Settings → Revenue Attribution. - Click the Paddle card.
- Paste the API key. Zenovay auto-detects Sandbox vs Live from the prefix (shown below the input).
- Click Connect.
- In
That's it. Behind the scenes, Zenovay:
- Validates the key against Paddle's
/event-typesendpoint. - Calls
POST /notification-settingsto create a webhook destination athttps://api.zenovay.com/api/webhooks/paddle/<your-website-id>subscribed to the four canonical events (transaction.completed,subscription.created,subscription.updated,subscription.canceled). - Captures
endpoint_secret_keyfrom Paddle's response and stores it encrypted. - 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)
- In Paddle → Notifications → Destinations → New destination → Webhook.
- URL:
https://api.zenovay.com/api/webhooks/paddle/YOUR_WEBSITE_ID(your website ID is the UUID in Settings → Websites inapp.zenovay.com). - Subscribe to All events (Zenovay silently ignores anything it doesn't handle).
- Save the destination.
- 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).
- 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. - Save.
For 99% of users, just use the Connect flow above and skip this.
Tag your transactions with the visitor ID (recommended)
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:
- In Paddle's sandbox dashboard, create a test product + price.
- Use a checkout overlay or hosted checkout URL on a test page that loads your Zenovay tracker.
- Complete the checkout with one of Paddle's test card numbers.
- Within ~10 seconds: Paddle sends a real, signed
transaction.completedevent 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 →
4999becomes49.99. - Zero-decimal currencies (JPY, KRW, VND, …): use the value as-is →
5000JPY stays5000. - Three-decimal currencies (BHD, KWD, OMR, …): divide by 1000 →
1500BHD becomes1.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:
- 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.
- 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_eventsrows for this website - Delete all Paddle
paymentsrows for this website - Clear the
paddle_customer_idfield 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:
- Open Paddle → Notifications → Destinations.
- Find the destination pointing at
https://api.zenovay.com/api/webhooks/paddle/{your-website-id}. - 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.updatedin your destination anyway — Zenovay silently ignores them today and will start rendering them as soon as V2 ships. subscription.past_dueandsubscription.pausedevents are received and dedup-recorded, but not yet rendered in the dashboard. Subscription status flips topast_duecorrectly, 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:
- 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. - 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.
- 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_eventsdirectly:SELECT * FROM payment_events WHERE payment_provider='paddle' ORDER BY created_at DESC LIMIT 5; - If
customer_emailis NULL on the row, attribution fell back topaddle_customer_idmatching. 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.