DOCS LLMs

Selling software licenses with Paddle

Selling software licenses with Paddle

Connect Paddle Billing to LicenseSeat and every completed payment issues a license key and emails it to the buyer. A subscription renewal extends the license; a refund or chargeback revokes it. Nothing to install on Paddle's side: the connection is a notification destination you create in your Paddle dashboard.

This guide is for Paddle Billing, Paddle's current platform. It does not apply to Paddle Classic.

Setup takes about five minutes.

Before you start

Link the product to a license plan before testing. Automatic license emails require an eligible LicenseSeat plan, Settings → Organization → Email license keys to my customers enabled, and a valid buyer email. Issuance can succeed without sending an email. Use a separate test organization when you need complete separation: connections in one organization share product mappings by provider.

Read Email delivery, subscription expiry and event recovery for the common prerequisites and recovery procedure. Setup instructions remain available on the connection page after saving.

1. Create the connection in LicenseSeat

In the LicenseSeat dashboard sidebar open Connected platforms, click Connect Your First Platform (or Connect Another), choose Paddle, and give the connection a name. The page shows your webhook URL. Copy it. Keep it private: only paste it into Paddle.

2. Create a notification destination in Paddle

In Paddle go to Paddle → Developer tools → Notifications and click New destination. Fill the dialog like this:

Field Value
Description any name, for example "LicenseSeat"
Notification type webhook
URL or email your LicenseSeat webhook URL from step 1
API version leave Paddle's default
Usage type Platform and simulation if you want to test with Paddle's simulator (step 6); otherwise Platform

Under the events list tick these five, then click Save destination:

  • transaction.completed — a payment finished processing. This is what issues (or renews) the license.
  • adjustment.created and adjustment.updated — refunds and chargebacks. These revoke the license.
  • customer.created and customer.updated — Paddle's transaction webhooks carry a customer ID but no email address, so LicenseSeat learns the buyer's email from these.

Do not use transaction.paid: Paddle fires it before it has finished processing the payment, and at that point a subscription may not yet have its subscription ID. transaction.completed is the one Paddle documents as "fully processed".

Paddle lets you create as many destinations as you like, but only 10 can be active at once.

3. Paste the signing secret

Back on the Notifications list, click the menu button next to the destination you just created and choose Edit destination. Click the copy icon in the secret key field; the key starts with pdl_ntfset_. Paste it into the Signing Secret field on the LicenseSeat connection page and save.

Paddle signs every webhook with this key and LicenseSeat rejects anything that is not signed with it. Each destination has its own key, so if you create a second destination later, use that one's key.

Sandbox and live are separate accounts. Paddle's sandbox dashboard is at sandbox-vendors.paddle.com, live is at vendors.paddle.com, and nothing is shared between them: not notification destinations, not secret keys, not products or price IDs. When you go live, create the destination again in your live account and paste its secret key into LicenseSeat (a second LicenseSeat connection for live keeps the two apart cleanly).

Tell LicenseSeat which license plan to issue for each Paddle price. On the connection page click Link Product, pick the plan, and enter the Paddle identifier:

  • Price ID (starts with pri_): in Paddle go to Catalog → Products, click the product, and under Prices copy the price's ID. Use this when a product has several prices (monthly and yearly, personal and studio) that should issue different licenses.
  • Product ID (starts with pro_): also accepted. Use it when the product has a single price. If both a price and its product are linked, the price wins.

Sandbox and live price IDs are different; link the ones from the account your destination lives in.

5. Optional: pass the buyer's email at checkout

If you open Paddle Checkout from your own site, include the buyer's email as custom data. LicenseSeat then has the email on the transaction itself and never has to wait for the customer webhook:

Paddle.Checkout.open({
  items: [{ priceId: "pri_...", quantity: 1 }],
  customer: { email: buyerEmail },
  customData: { email: buyerEmail }
});

On an HTML checkout button the equivalent is data-custom-data='{"email":"..."}'. LicenseSeat reads the keys email, customer_email or buyer_email. Paddle copies a subscription's custom data onto every transaction it creates for it (renewals, upgrades, one-time charges), so one checkout covers the whole life of the subscription.

If you use Paddle's hosted checkout links or don't control the checkout, skip this step: the customer.created / customer.updated events from step 2 supply the email.

6. Test it

Paddle can send a signed test webhook without a real payment. In Paddle go to Developer tools → Simulations and click New simulation. Choose your destination in the Destination dropdown, give the simulation a name, open the Single event tab, pick transaction.completed, and click Create. Click Run simulation.

Paddle's sample payload uses a demo price and a demo customer, so edit it: open the Payload tab in the preview pane, click the payload to edit it, set items[0].price.id to a price you linked in step 4, add "custom_data": {"email": "[email protected]"}, and click Replay. Within a few seconds the LicenseSeat connection shows Active and a license appears under your product, emailed to that address. If the sample's origin is not web, change it to web as well; LicenseSeat only fulfils checkout, API and one-time-charge origins.

This needs the destination's Usage type to be Platform and simulation; a Platform-only destination never receives simulated events. Simulations work in both sandbox and live accounts, and their webhooks are signed with the destination's real secret key, so a signature failure here means the secret in LicenseSeat is wrong.

For a full rehearsal, use a sandbox account: buy the product through your own checkout with card 4242 4242 4242 4242 (any name, any future expiry). Then in Paddle go to Transactions, open the transaction, click Refund, enter the amount, click Continue, then Request refund. Sandbox approves refunds automatically within about ten minutes. On the connection's event list you should see the license issued, emailed, and then revoked when the approval arrives.

What happens on each event

Paddle event LicenseSeat
transaction.completed with origin web, api or subscription_charge Issues a license for the first item that matches a linked price (or its product), owned by the buyer's email, and emails the key. Quantity follows the item.
transaction.completed with origin subscription_recurring Renews the license issued for that subscription to the end of the new billing period.
transaction.completed with origin subscription_update or subscription_payment_method_change Ignored. Plan changes, prorations and card updates do not issue or extend anything.
adjustment.created / adjustment.updated with action refund, status approved Revokes all licenses correlated with the transaction or subscription. Any approved partial refund revokes too, including adjustments for another item in the transaction.
adjustment.created with action chargeback Revokes the license issued for that transaction.
adjustment.* with status pending_approval or rejected Nothing. Most live refunds start as pending_approval and are reviewed by Paddle; the revoke happens when adjustment.updated arrives with approved.
customer.created / customer.updated Stored so the buyer's email can be matched to their transactions. Shown as ignored on the event list, which is expected.
subscription.canceled Ignored. The license simply is not renewed; it expires at its stored expiry. Initial access uses the mapped plan duration, which must match the billing interval; later renewals use the provider’s billing period end.
Anything else Ignored. Subscribing to extra events does no harm.

Good to know

  • Existing perpetual mappings. Subscription purchases still issue on a perpetual plan. Events that identify a subscription show a warning; cancellation does not revoke lifetime access. Existing perpetual licenses remain perpetual on renewal. Choose a finite plan for access that should expire, and review subscription access and recovery before changing existing mappings.

  • Finite subscription plans. Choose a license plan with a duration matching the billing interval. Initial issuance uses this duration; it does not synchronize a trial or initial provider period. Subsequent renewals use the transaction billing period end. Verify both stages before selling subscriptions.

  • Buyer email. Paddle's transaction webhooks carry a customer_id and no email. LicenseSeat looks in the transaction's custom data first (step 5), then in the customer.created / customer.updated events it has stored. If neither has arrived yet, the event waits and retries with finite automatic attempts; retained missing-history failures are eligible for recovery when newer provider history arrives; Paddle does not guarantee webhook order, so the customer event can land after the transaction. Replay the relevant customer notification in Paddle; a no-op save is not a guaranteed new event.

  • Existing customers. customer.created only fires for customers created after you connected. Renewals for subscriptions that predate the connection have no stored customer event and no license to renew; they show as failed or ignored. Issuing a license manually with the same email does not attach the Paddle subscription ID. Future renewals still cannot match it. Replay the original purchase with its subscription/customer history, or manage that license manually; automatic import of pre-existing subscriptions is not provided.

  • Live refunds need Paddle's approval. Paddle approves automatically only for verified, active accounts when the refund is 400 USD or less and below your balance; otherwise a person at Paddle reviews it. The license stays valid until the approval webhook arrives.

  • Retries. Paddle expects a 200 within five seconds and retries with exponential backoff: 3 times within 15 minutes in sandbox, 60 times over 3 days on live. LicenseSeat answers immediately and processes in the background, and duplicate deliveries are recognised by Paddle's event_id, so a payment is never fulfilled twice.

  • First matching item, matching quantity. Try each item in order, its Price ID before its Product ID, and issue that item’s quantity of licenses (1–1000). Later items are not fulfilled. Use separate checkouts for separately licensed products.

  • Signature timing. Paddle's own SDKs reject webhooks whose timestamp is more than five seconds old. LicenseSeat allows five minutes, so a slow hop or a little clock drift on Paddle's side does not turn into a rejected payment.

Troubleshooting

  • The connection stays "Incomplete". The signing secret is missing, or no price is linked yet. Both are on the connection page.
  • Paddle shows failed deliveries (HTTP 401). The signing secret does not match. Open the destination (menu → Edit destination) and copy the secret key again. Remember sandbox and live destinations have different keys.
  • The event shows as "ignored" with "no active mapping". The transaction's price ID does not match any linked product. Compare the pri_ in the event payload with what you linked; sandbox IDs never match live ones. Simulated events use Paddle's demo price unless you edit the payload (step 6).
  • The event keeps retrying, or fails with "awaiting customer.created/updated". LicenseSeat has no email for that customer. Check that customer.created and customer.updated are ticked on the destination, or pass the email as custom data at checkout (step 5). For a simulated event, add custom_data.email to the payload.
  • A renewal shows as "ignored". Its origin was subscription_update or subscription_payment_method_change (a plan change or card update), which is by design. Only subscription_recurring renews.
  • A refund did not revoke. The refund is probably still pending_approval; the revoke arrives with adjustment.updated. Make sure that event is ticked on the destination. Refunds of transactions made before the connection existed cannot be matched.
  • Paddle will not let you save a new destination. Only 10 destinations can be active at once; deactivate an old one.
  • Simulations arrive but real purchases don't (or the reverse). Check the destination's Usage type: Platform receives only real events, Simulation only simulated ones, Platform and simulation both.

References

These are Paddle's own pages. If a label or menu in the Paddle dashboard ever differs from this guide, they are the source of truth:

  • Create or update notification destinations — Developer tools → Notifications, the New destination dialog fields (Description, Notification type, URL or email, API version, Usage type), Save destination, Edit destination, the 10-active-destinations limit.
  • Verify webhook signatures — where the pdl_ntfset_ secret key is (Edit destination → secret key field), the Paddle-Signature header (ts / h1), HMAC-SHA256 over ts:body, the five-second SDK tolerance.
  • Handle webhook delivery — respond with 200 within five seconds; retry schedule (sandbox 3 in 15 minutes, live 60 over 3 days); replaying failed notifications.
  • How webhooks work — at-least-once delivery, event_id deduplication, occurred_at ordering, when to use transaction.completed.
  • Simulate webhooks — Developer tools → Simulations, New simulation, Single event / Scenario, Run simulation, Replay, editing the Payload tab, the Usage type requirement, signed simulated events.
  • Webhook simulator overview — works in sandbox and production.
  • transaction.completed — when it fires, the origin values, customer_id, custom_data, items[].price.id, items[].price.product_id.
  • transaction.paid — why it is not used for fulfilment.
  • adjustment.created and adjustment.updated — refund and chargeback actions, pending_approvalapproved / rejected.
  • Refund or credit a transaction — Transactions → Refund → Continue → Request refund; automatic approval rules; sandbox auto-approval every ten minutes.
  • customer.created and customer.updated — the events that carry the buyer's email.
  • subscription.canceled — immediate vs. end-of-period cancellation.
  • Subscription renewed scenario — the seven events of a renewal; origin is subscription_recurring.
  • Work with custom datacustomData in Paddle.Checkout.open(), the data-custom-data attribute, custom data copied between subscriptions and their transactions.
  • Paddle.Checkout.open()items[].priceId, customer.email, customData.
  • Create products and prices — Catalog → Products → New product; Prices → New price; pro_ and pri_ IDs.
  • Sandbox — separate sandbox and live accounts (sandbox-vendors.paddle.com vs vendors.paddle.com), nothing shared between them, test card numbers, go-live checklist.
  • Provision access with webhooks — Paddle's general guidance for SaaS access control (subscription.created / subscription.updated); LicenseSeat issues per payment, so it uses transaction.completed instead.