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:
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
webhookUrlfrom step 1. - Events: select
customer.created,customer.updatedtransaction.completedsubscription.updated,subscription.past_due,subscription.canceled,subscription.paused,subscription.resumed,subscription.activatedadjustment.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:
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:
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 event | Effect on the license |
|---|---|
customer.created, customer.updated | Create or update the customer (email, name) linked to the Paddle customer_id. No license change. |
transaction.completed with a subscription | Find or create the license linked to the subscription_id; paidUntil = billing_period.ends_at; status active; seats = item quantity. |
transaction.completed without subscription | One-time purchase: a perpetual license. |
subscription.activated, subscription.resumed | Status active. |
subscription.past_due | Status past_due. |
subscription.canceled | Status canceled (usable until validUntil). |
subscription.paused | Status suspended. |
subscription.updated | Status follows the subscription’s status, with the same mapping. Dates are not changed. |
adjustment.created / adjustment.updated, action = refund, status = approved, full amount | Status refunded. |
adjustment.created / adjustment.updated, action = chargeback | Status 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.
Need help? See Troubleshooting or write to support@keyrail.tech.