
Paddle Webhooks vs Database Sync: Which is Better?
Comparing Paddle webhooks and database sync for getting billing data into PostgreSQL. Learn when to use each approach, and when to use both.
If you're building on top of Paddle Billing, you'll eventually need that data in your own database, to power dashboards, reconcile revenue, or join billing data against your product tables. There are two main ways to get it there: webhooks and database sync. Both work, but they solve different problems.
This post breaks down how each approach works with Paddle specifically, what tends to go wrong, and when you should reach for one over the other.
How Paddle Webhooks Work
Paddle webhooks (Paddle calls them notifications) are push-based. You create a notification destination in the Paddle dashboard, and when something happens, a transaction completes, a subscription is cancelled, a customer is created, Paddle sends an HTTP POST to your endpoint with the event payload.
Here's a typical handler in Express using the official Paddle Node SDK:
import express from 'express';
import { Paddle, EventName } from '@paddle/paddle-node-sdk';
const paddle = new Paddle(process.env.PADDLE_API_KEY);
const webhookSecret = process.env.PADDLE_WEBHOOK_SECRET;
app.post(
'/webhooks/paddle',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['paddle-signature'] as string;
const rawBody = req.body.toString();
let event;
try {
// Verifies the Paddle-Signature header and parses the payload
event = await paddle.webhooks.unmarshal(
rawBody,
webhookSecret,
signature,
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.eventType) {
case EventName.TransactionCompleted:
// Insert/update your transactions table
break;
case EventName.SubscriptionUpdated:
// Update your subscriptions table
break;
case EventName.CustomerCreated:
// Insert into your customers table
break;
// ... handle dozens more event types
}
res.status(200).send('ok');
},
);
That's the gist. Paddle pushes events to you in near real-time, and your server processes them.
The Problems with Paddle Webhooks
Webhooks are great in theory. In practice, Paddle's push model comes with a familiar list of operational headaches:
Signature verification with a timestamp window. Paddle signs each request with the Paddle-Signature header, which contains a timestamp (ts) and an HMAC-SHA256 hash (h1). You have to rebuild the signed payload as ts:body, hash it with your endpoint secret, and compare — while also rejecting stale timestamps to prevent replay attacks. Get the raw-body handling wrong (parse the JSON too early and the bytes no longer match) and every signature fails.
Sandbox and live are completely separate. Paddle runs sandbox and production as isolated environments with different API keys and different webhook secrets. The classic outage: everything works in sandbox, you go live, and prod silently drops every event because the endpoint is still pointed at the sandbox secret.
Missed events during downtime. If your endpoint is down or returns a non-2xx, Paddle retries notifications with backoff for up to 3 days. Survive a longer incident, a bad deploy, or a changed endpoint URL, and those events are gone unless you backfill from the API.
No historical backfill. Notifications only capture events from the moment the destination is created. Want last year's transactions? Webhooks can't help, you'll write a separate backfill script against the Paddle API.
Schema management across event types. transaction.completed, subscription.updated, adjustment.created, customer.updated and friends each carry a different payload shape. Supporting them means a lot of mapping logic to write and keep current as Paddle evolves the API.
Replay complexity. Need to rebuild your data after a bug? Paddle's dashboard lets you replay notifications, but doing it at scale is fiddly, and there's no native "resync everything from scratch" button.
How Database Sync Works
Database sync takes the opposite approach. Instead of Paddle pushing events to you, a sync service periodically pulls data from the Paddle API and writes it directly into your PostgreSQL database.
The flow looks like this:
- Connect your PostgreSQL database (Supabase, Neon, Railway, AWS RDS, or any PostgreSQL host)
- Provide your Paddle API key (read access)
- The sync service calls the Paddle API, fetches your data, and writes it to structured tables
- On subsequent runs, it pulls what changed and upserts, no duplicates, no gaps
There's no notification endpoint to expose, no Paddle-Signature to verify, no sandbox/live secret mismatch to debug. The data just shows up in your database, ready to query.
Side-by-Side Comparison
| Factor | Paddle Webhooks | Database Sync |
|---|---|---|
| Data freshness | Near real-time (seconds) | Batch (e.g. every 6 hours / daily) |
| Setup complexity | High — endpoint, signature + timestamp checks, event handling | Low — connect database and API key |
| Historical data | No — only captures new events | Yes — full backfill on first sync |
| Sandbox vs live | Separate secrets, easy to misconfigure | One API key per environment, no endpoint to mismatch |
| Reliability | You handle retries, idempotency, failures | Managed by the sync service |
| Maintenance | Ongoing — new event types, API changes | Minimal — schema handled for you |
| Code required | Significant — handler, mapping, verification | None (no-code) or minimal |
| Best for | Triggering actions in real-time | Querying and analysing data |
When to Use Paddle Webhooks
Webhooks are the right choice when you need to react to events in real-time:
- Provision access the moment
subscription.activatedfires - Revoke access when
subscription.canceledortransaction.payment_failedlands - Send a receipt or welcome email when a transaction completes
- Trigger a churn survey when a subscription is cancelled
- Update a UI instantly when a payment succeeds
If your use case is "when X happens in Paddle, do Y immediately," webhooks are what you want. The real-time push model is built for exactly this.
When to Use Database Sync
Database sync is the right choice when you need to query, analyse, or join Paddle data:
- Build dashboards showing revenue trends, MRR, churn, or customer growth
- Run ad-hoc queries like "which customers have spent over £1,000 this quarter?"
- Join billing data with your app data, combine Paddle customers with your users table
- Generate reports for accounting or investor updates
- Power internal tools where teams need to look up billing history
Once your Paddle data is synced to PostgreSQL, you can run queries like this:
-- Monthly completed-transaction revenue for the last 6 months
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS transaction_count,
SUM(grand_total) / 100.0 AS revenue
FROM paddle_transactions
WHERE status = 'completed'
AND created_at >= NOW() - INTERVAL '6 months'
GROUP BY month
ORDER BY month DESC;
-- Active subscriptions joined to your own users table
SELECT u.email, ps.status, ps.next_billed_at
FROM users u
JOIN paddle_customers pc ON pc.email = u.email
JOIN paddle_subscriptions ps ON ps.customer_id = pc.id
WHERE ps.status = 'active'
ORDER BY ps.next_billed_at;
Try doing that against the Paddle API directly. You'd need multiple paginated calls, client-side filtering, and careful rate-limit handling. With synced data, it's just SQL.
When to Use Both
Here's the thing, webhooks and database sync aren't mutually exclusive. The best setups often run both:
- Webhooks handle real-time events: provision access, send emails, trigger workflows
- Database sync keeps a queryable, reconciled copy of your Paddle data for reporting and analysis
Your app reacts to events instantly through webhooks, while your team runs any query it wants against the synced database. The common mistake is using webhooks for everything, including analytics workloads that don't need sub-second freshness and pay for it in maintenance.
Getting Started with Database Sync
If you want to try the sync approach, Codeless Sync connects your PostgreSQL database and syncs Paddle data, customers, subscriptions, transactions, products, prices, adjustments, and discounts, in about 5 minutes. It auto-creates the destination tables, upserts on every run so there are no duplicates, and recovers from downtime automatically on the next scheduled sync. There's a free tier, no credit card required.
The same model works for Stripe, QuickBooks, and Xero too, so if you bill across more than one provider, all of it lands in the same Postgres database.
Frequently Asked Questions
Does Paddle have a built-in PostgreSQL integration?
No, Paddle doesn't ship a native sync to PostgreSQL or any other database. The two official ways to get data out are webhooks (push, real-time, you build the handler) and the Paddle API (pull, on-demand, you build the polling logic). Everything else is third-party. To get Paddle data into your own Postgres for analytics or accounting, you either write your own pipeline or use a sync tool like Codeless Sync.
Why does my Paddle webhook keep failing?
The most common causes are signature verification failures (the raw request body was modified or parsed before hashing, so the Paddle-Signature no longer matches), a sandbox/live secret mismatch after going to production, or an endpoint that returned a non-2xx long enough for Paddle to exhaust its retry window. Paddle's dashboard shows the delivery log and response code for each notification, start there.
Can I replace Paddle webhooks entirely with a scheduled sync?
For analytics, MRR dashboards, accounting, reporting, and most CRM workflows, yes, a scheduled sync against the Paddle API is more reliable than a custom webhook handler and needs no public endpoint. For real-time use cases (instant access provisioning, immediate payment confirmation UX), you'll still want webhooks for those specific events. Many teams run both: webhooks for the few events that need real-time response, scheduled sync for everything else.
How often should I sync Paddle to PostgreSQL?
For analytics and reporting, a 6-hourly or daily sync is usually plenty. Accounting workflows that close books daily are well served by a daily sync. Real-time freshness is rarely needed for these workloads and just means more API calls. Codeless Sync supports manual syncs on the free tier; paid tiers add scheduled cadences.
Which Paddle data can I sync to my database?
Codeless Sync supports seven Paddle Billing data types: customers, subscriptions, transactions, products, prices, adjustments (refunds, credits, chargebacks), and discounts. Transactions support incremental sync, so each run only pulls what changed since the last one.
Related:
Questions or feedback? Feel free to reach out. If you found this helpful, you can try Codeless Sync for free.