Payments

Connect Paddle Billing

Create licenses automatically from Paddle Billing transactions, subscriptions and refunds.

Paddle Billing handles checkout, taxes and invoices but has no license keys. Once connected, Keyrail creates, renews and revokes licenses from Paddle’s webhooks, and emails the key to your customer.

1. Create the integration#

In the dashboard, open Integrations in the sidebar and click Connect provider. Choose Paddle Billing, the product, and the Environment (Sandbox (test payments) to start, Live for real sales), then Connect. The integration then shows its Webhook URL, the Webhook secret field and the Price map (dashboard guide). Or with the vendor API:

bash
curl https://api.keyrail.tech/v1/integrations \
  -H "Authorization: Bearer $KEYRAIL_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "paddle",
    "product": "prd_7k2m9q4x1c8d3f6h0j5n",
    "environment": "sandbox",
    "priceMap": {
      "pri_01hv8x2monthly000000000000": "pro",
      "pri_01hv8x2yearly0000000000000": "pro",
      "pri_01hv8x2lifetime00000000000": "lifetime"
    }
  }'

The response contains a webhookUrl of the form https://api.keyrail.tech/v1/webhooks/paddle/int_…. Keep this page open.

In the dashboard, map prices in the integration’s Price map: Add price, paste the Paddle price ID, pick the plan, then Save price map.

priceMap maps each Paddle price ID (pri_…) to the code of one of your plans. Several prices can point to the same plan (monthly and yearly, different currencies). environment is sandbox or live: use one integration per Paddle environment.

2. Add a notification destination in Paddle#

In Paddle, go to Developer tools → Notifications → New destination:

  • Type: webhook. URL: the webhookUrl from step 1.
  • Events: select
    • customer.created, customer.updated
    • transaction.completed
    • subscription.updated, subscription.past_due, subscription.canceled, subscription.paused, subscription.resumed, subscription.activated
    • adjustment.created, adjustment.updated

Save, then copy the destination’s secret key and store it in Keyrail: paste it in the integration’s Webhook secret field and click Save secret, or with the API:

bash
curl -X PATCH https://api.keyrail.tech/v1/integrations/int_4w8d2k6m0p3r7t1v5x9z \
  -H "Authorization: Bearer $KEYRAIL_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhookSecret": "pdl_ntfset_…" }'

The secret is write-only: no API returns it.

3. Customer emails (and the optional API key)#

Paddle’s transaction and subscription events carry a customer_id, not the customer’s email. Keyrail learns the email (and name) from the customer.created and customer.updated events, which is why they must be enabled.

Paddle does not guarantee delivery order. If a transaction.completed arrives before Keyrail knows its customer, it answers 500 and Paddle retries a little later, once customer.created has been processed. The license is created on that retry.

To avoid the wait, you can give Keyrail a Paddle API key with the read-only customer.read permission. Keyrail then fetches the unknown customer from the Paddle API right away. The key is write-only and stored encrypted; responses only show hasApiKey: true. In the dashboard, paste it in the integration’s Paddle API key field and click Save key; or with the API:

bash
curl -X PATCH https://api.keyrail.tech/v1/integrations/int_4w8d2k6m0p3r7t1v5x9z   -H "Authorization: Bearer $KEYRAIL_SECRET_KEY"   -H "Content-Type: application/json"   -d '{ "apiKey": "pdl_sdbx_apikey_…" }'

4. Test with the Paddle sandbox#

Use a sandbox checkout with a test card. Within seconds, the license appears in Licenses, the webhook appears in Events and in the license’s History, and the customer receives the key by email. Nothing happened? See no license after a payment. Before real sales, go through the going-live checklist.

What each event does#

Paddle eventEffect on the license
customer.created, customer.updatedCreate or update the customer (email, name) linked to the Paddle customer_id. No license change.
transaction.completed with a subscriptionFind or create the license linked to the subscription_id; paidUntil = billing_period.ends_at; status active; seats = item quantity.
transaction.completed without subscriptionOne-time purchase: a perpetual license.
subscription.activated, subscription.resumedStatus active.
subscription.past_dueStatus past_due.
subscription.canceledStatus canceled (usable until validUntil).
subscription.pausedStatus suspended.
subscription.updatedStatus follows the subscription’s status, with the same mapping. Dates are not changed.
adjustment.created / adjustment.updated, action = refund, status = approved, full amountStatus refunded.
adjustment.created / adjustment.updated, action = chargebackStatus revoked.

Subscription events older than the license’s last processed event are ignored, so out-of-order delivery is safe. Partial refunds don’t change the license.

The customer is found through the Paddle customer_id (see customer emails), then matched by email within your account, or created. One customer with one subscription to a plan has one license: re-subscribing after expiry reuses the same license and key.

Signature verification#

Keyrail checks the Paddle-Signature header (ts=…;h1=…): h1 must equal the HMAC-SHA256 of ts:rawBody with your secret, and ts must be within 5 minutes. Anything else gets 401. Details in webhooks.

View as Markdown

Need help? See Troubleshooting or write to support@keyrail.tech.