Synchronizing Paddle Billing Transactions

A note from the founder. Running a SaaS on Paddle and tired of manually exporting transaction CSVs? I'm looking for a small group of early users to try Runhooks and share honest feedback. Early adopters get upgraded plans for free.

You chose Paddle so you wouldn't have to think about VAT, sales tax, or currency conversion. As your Merchant of Record, Paddle handles all of that — collecting payments from customers worldwide, remitting taxes to the right jurisdictions, and paying you out in your preferred currency.

But your internal systems still need that transaction data. Your finance team wants revenue breakdowns by country. Your BI dashboard needs MRR and churn metrics that Paddle's UI doesn't surface. And your accounting tool needs line-item records that match what Paddle reports, without someone copy-pasting from a CSV export every week.

The solution is a scheduled sync: a job that pulls yesterday's transactions from Paddle's API and writes them into your database. The kind of task that should run every day at 6 AM and never require human intervention.

Why Manual Exports Break Down

Most SaaS founders start with manual workflows. You log into Paddle's dashboard, export transactions for the past week, open a spreadsheet, and reconcile against your internal records. This works when you have 20 customers.

At 200 customers across 15 countries, it doesn't:

  • Currency inconsistency — Paddle collects in the customer's local currency but pays you out in yours. A manual export contains both, and reconciling them means tracking Paddle's exchange rates for each transaction.
  • Tax complexity — Each transaction includes tax amounts that vary by country, state, and product type. Your accounting system needs the net amount after tax, but the export gives you gross and tax as separate columns that need mapping.
  • Timing gaps — Exports reflect the moment you pulled them. Refunds, chargebacks, and adjustments that happen after your export are missing until the next manual pull.
  • Human error — Someone eventually pastes the wrong date range, skips a week during vacation, or accidentally imports the same batch twice. You don't find out until the quarterly reconciliation.

The Merchant of Record model solves payment complexity for your customers. It doesn't solve data complexity for your internal systems.

What Paddle's API Gives You

Paddle's Billing API exposes a Transactions endpoint that returns structured data for every completed transaction — including the transaction status, customer reference, the currency collected from the buyer, gross/tax/net breakdowns, and crucially, the payout totals already converted to your home currency.

That payout totals field is what makes automated reconciliation practical. Instead of tracking exchange rates yourself, you use Paddle's calculated payout amounts — the numbers that will actually appear in your bank account.

The API supports filtering by date range, cursor-based pagination, and including related entities. See Paddle's API reference for the full list of fields and query parameters. This gives you everything needed to build a daily sync without touching the dashboard.

Building the Sync Endpoint

The pattern is the same as any scheduled HTTP task: expose an endpoint in your backend that performs the sync, and call it on a schedule.

// POST /api/billing/sync-transactions
async function handleTransactionSync(req, res) {
  // 1. Calculate yesterday's date range in UTC

  // 2. Fetch completed transactions from Paddle's List Transactions API
  //    Filter by created_at date range, paginate through all results
  //    See: https://developer.paddle.com/api-reference/transactions/list-transactions

  // 3. Upsert each transaction into your database
  //    Store: transaction ID, customer, status, currency, gross/tax/net amounts,
  //    payout currency + payout amount (for revenue in your home currency)
  //    Use upsert (INSERT ... ON CONFLICT UPDATE) so re-runs are safe

  // 4. Return a summary: { synced: count, date: "YYYY-MM-DD" }
}

The important design decisions:

  • Pagination — Paddle's API is paginated. Follow the cursor until there are no more pages, especially as transaction volume grows. See Paddle's pagination docs.
  • Upsert, not insert — use an upsert so that re-running the sync for the same day is safe. If a transaction was updated between runs, the record gets refreshed rather than duplicated.
  • Store payout totals — Paddle converts amounts to your payout currency for you. Storing both the original currency and the payout amount means your BI queries can group revenue in your home currency without manual conversion.

Handling Refunds and Adjustments

A daily sync that only pulls completed transactions misses an important class of events: refunds and partial refunds that happen after the original transaction date.

The fix is a second pass in the same sync job — query Paddle's List Transactions endpoint again, but this time filter by updated_at instead of created_at. This catches any transaction that was modified yesterday (refunded, partially refunded, or adjusted), regardless of when it was originally created.

async function handleTransactionSync(req, res) {
  // Pass 1: Fetch transactions CREATED yesterday (new sales)
  // Pass 2: Fetch transactions UPDATED yesterday (refunds, adjustments)
  // Upsert both — duplicates are handled by the ON CONFLICT clause
}

Because you're upserting, transactions that appear in both passes are harmlessly deduplicated. This ensures your database reflects refunds and chargebacks within 24 hours of Paddle processing them, without a separate reconciliation workflow. For more on how Paddle handles adjustments, see their Adjustments API.

Why This Needs a Reliable Scheduler

A billing sync that silently fails is worse than no sync at all. Your finance team starts trusting the internal data, makes decisions based on it, and doesn't realize it's three days stale until the numbers don't add up.

A cron job on your server can trigger the endpoint, but it brings familiar problems:

  • No failure notification — if Paddle's API returns a rate limit error or your database connection drops, cron exits and moves on. You won't know until someone checks.
  • No retry logic — a transient 429 resolves itself in seconds, but cron waits 24 hours to try again. That's a full day of missing data.
  • No execution history — there's no record of how many transactions were synced, how long the job took, or whether sync duration is trending upward — a signal that transaction volume is growing and your sync might need tuning.
  • Infrastructure coupling — the cron schedule is tied to a specific server. A deployment, migration, or auto-scaling event can silently delete it.

For a job that feeds your accounting and BI systems, you need visibility into every run and automatic recovery from transient failures.

Scheduling With Runhooks

Runhooks calls your sync endpoint on a schedule with built-in retries, logging, and alerting. Setup:

  1. Create a job — "Daily Paddle Transaction Sync"
  2. Set the URLhttps://api.yourapp.com/api/billing/sync-transactions
  3. Set the schedule0 6 * * * (daily at 6:00 AM)
  4. Set the timezone — your business's primary timezone (e.g. Europe/Berlin, America/New_York)
  5. Add an auth headerAuthorization: Bearer <your-sync-secret>
  6. Enable retries — 3 attempts with exponential backoff

What this gives you:

  • Execution logs — every sync run is recorded with HTTP status, response body (including synced count), and duration. You can see that Monday synced 147 transactions in 3.2 seconds and Tuesday synced 203 in 4.8 seconds.
  • Automatic retries — if Paddle returns a 429 or your database is temporarily unreachable, Runhooks retries within seconds instead of waiting until tomorrow.
  • Failure alerts — if the sync fails after all retries, you get an email or webhook notification. Your finance team doesn't discover stale data during the monthly close.
  • Timezone-aware scheduling — set the job to run at 6 AM in Europe/Berlin and it stays at 6 AM through DST transitions. Paddle transactions from yesterday in your timezone are always captured.
  • No infrastructure coupling — the schedule lives in Runhooks, not on your application server. Deployments, container restarts, and scaling events don't affect it.

Extending the Pattern

Once the daily transaction sync is running, the same approach works for other Paddle data your internal systems need:

  • Subscription sync — pull Paddle's Subscriptions API to track active, paused, and cancelled subscriptions for churn analysis
  • Customer sync — pull the Customers API to keep your CRM or user database aligned with Paddle's customer records
  • Invoice retrieval — use the Get invoice endpoint to archive PDF invoices in your document storage
  • Payout reconciliation — pull the Payouts API to match Paddle's payouts against your bank statements

Each is a separate endpoint in your backend and a separate job in Runhooks — with its own schedule, retry policy, and alerting.

Get Started

Paddle handles the hard parts of global payments — tax calculation, currency conversion, and regulatory compliance. Your internal systems just need the data:

  1. Build a sync endpoint that pulls from Paddle's /transactions API and upserts into your database
  2. Try Runhooks and schedule it to run daily in your business timezone
  3. Get execution logs, retries, and alerts — so you know when a sync fails before your finance team does

Preview your schedule with the cron expression visualizer, and compare plans when you need more jobs or longer log retention.

Read next: Why Cron Jobs Fail in Production · Scheduled HTTP Requests vs. Cron Jobs · Offloading Heavy Database Maintenance to Off-Peak Hours