Selling through Paddle

KeyGrant creates a license key for each paid checkout on your own Paddle account (Paddle Billing). Payments go to your Paddle account, through a checkout page of yours. A KeyGrant account sells through Paddle or through Stripe, one at a time.

At a glance

Linked with
An API key you paste in Settings > Payments
Environments
Live and sandbox, each with its own key
Your checkout page
Built with Paddle.js, passing client_reference_id
A refund
Counts once Paddle approves it
A sale while Paddle is unlinked
Paddle retries it for about 2 days

1. Make an API key in Paddle

In Paddle, open My account > Settings > Authentication and make a key on the API keys tab. The page is vendors.paddle.com/settings/authentication for your live account and sandbox-vendors.paddle.com/settings/authentication for the sandbox. Give the key these permissions:

  • transaction.read
  • customer.read
  • subscription.read
  • adjustment.read
  • adjustment.write
  • price.read
  • price.write
  • product.read
  • notification_setting.read
  • notification_setting.write
  • customer_portal_session.write
  • checkout_settings.read

KeyGrant refuses a key at once when it cannot list or make the notification destination, and one that lacks another permission fails when that is first used. A key can expire: make a new one before it does (see When Paddle refuses the key).

  1. Open Settings > Payments (Settings is the gear at the top right of the dashboard).
  2. Under Paddle, paste the key and choose Link Paddle.
  • You don't need to add a notification destination in Paddle. KeyGrant makes one under your key, for these events only: transaction.completed, subscription.updated, subscription.canceled, adjustment.created and adjustment.updated.
  • A live key links your live account and a sandbox key links your sandbox, which is a Paddle account of its own. Link both to test before you go live. A key made before May 2025 names no environment, so KeyGrant tries it live and then in the sandbox.
  • KeyGrant stores the key encrypted and shows only its last four characters. Replace key swaps it. A key for the same Paddle account keeps the link, and a key for another Paddle account links that one and lets the first go (see Unlink Paddle).
  • KeyGrant creates keys only for checkouts on the Paddle accounts linked there.
  • A Paddle account can be linked to one KeyGrant account at a time.
  • Stripe must be disconnected first, and Paddle unlinked before Stripe can be connected.

3. Map prices to license models

  1. On a product's License models tab, open a model's menu and choose Edit model.
  2. Under Price, pick the Paddle price (pri_…) that sells it. Its amount, currency and renewal interval come from Paddle, so there is nothing to type.
ModelPaddle price
Perpetual or timedA one-time price.
SubscriptionA monthly or yearly price.
Trial Processor price it maps to, in place of Price: the Paddle price a customer pays to buy the app after the trial.
  • Each model maps to one price. Settings > Payments lists every mapping under Price to license model.
  • Paddle has no search for KeyGrant to ask. The picker reads your 200 newest active prices and matches what you type against the product's name, the price's name or description, and the ID. Paste a whole pri_… ID to find any price, archived or not.
  • The picker lists the live account's prices and then the sandbox's. A model maps to one ID, and the live and sandbox IDs differ (see Test in the sandbox).

A paid checkout on a trial model's price creates a paid key of that model, never a trial key (see Trial keys).

Paddle opens no checkout at all, hosted or with Paddle.js, until the account has a default payment link. A checkout without one fails with transaction_default_checkout_url_not_set. Set it in Paddle, under Checkout > Checkout configuration, in Default payment link.

  • Your live account and your sandbox each need their own.
  • The link must be https, and not localhost.
  • In the sandbox, its domain needs no approval. In your live account, its host must be a domain Paddle has approved, exactly, or its www. version.
  • Paddle opens a checkout by sending the buyer to that link with ?_ptxn=<transaction ID> added, so the page must load Paddle.js. The checkout page below can be that page.
  • Settings > Payments warns under a link while its account has none. It can only check with a key that has checkout_settings.read, and says so when the key lacks it.

5. Build your checkout page

Paddle checkouts open from a page of yours, with Paddle.js. The page does one thing KeyGrant needs: it passes the client_reference_id it was opened with to Paddle.js as customData.

Your checkout page
// Load Paddle.js on the page: <script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
// The address your app opened is yours: upgradeUrl() added client_reference_id to it.const reference = new URLSearchParams(location.search).get('client_reference_id')
Paddle.Environment.set('sandbox') // leave this line out when you sell for realPaddle.Initialize({ token: 'YOUR_CLIENT_SIDE_TOKEN', eventCallback: (event) => { if (event.name === 'checkout.completed') { // data.transaction_id is the txn_… ID your success page looks the license up by. location.href = '/thanks?txn=' + encodeURIComponent(event.data.transaction_id) } }})
Paddle.Checkout.open({ items: [{ priceId: 'pri_…', quantity: 1 }], // The only checkout custom data KeyGrant reads. customData: reference ? { client_reference_id: reference } : undefined})
  • The token is a client-side token, made on the same Authentication page. Never put the API key you gave KeyGrant in a page.
  • upgradeUrl(link) in the SDKs adds client_reference_id=<activation ID> to the link and changes nothing else. Point it at this page, and the page reads the value from its own address, as above.
  • For an upgrade, client_reference_id names the key the sale changes. For anything else it does nothing. A buyer who arrives without one buys as a new customer, so open the checkout without customData then.
  • KeyGrant reads client_reference_id and nothing else from the checkout's custom data, because it is set in the buyer's browser. The sale's other settings come from the price (see Settings on a price).

6. Show the buyer their key

Paddle.js reports the transaction in checkout.completed as data.transaction_id (txn_…). Send the buyer to your success page with it, and request the license from your server with that ID:

Terminal
curl https://api.keygrant.dev/v1/checkouts/txn_01h…/license \  -H "Authorization: Bearer $KEYGRANT_SECRET"
  • Retry for a few seconds on 404 unknown-checkout: the notification that creates the key can arrive a moment after the buyer does.
  • The answers, and why the key must be a kg_live_ one, are on After a checkout.
  • Paddle gives a notification 5 seconds to be answered. KeyGrant reads the transaction from Paddle while it answers, so when Paddle's API is slow or limits KeyGrant it answers with an error and Paddle sends the notification again. The key is created on the redelivery.

What a sale creates

A transaction a buyer starts at your checkout page (or you start over the Paddle API) and that Paddle completes creates a key, when it includes a mapped price. KeyGrant reads the transaction back from Paddle with your key, and creates nothing for one Paddle has no completed record of.

  • Nothing to pay: a free trial, or a checkout discounted to nothing, creates its key as a sale of nothing.
  • A renewal or a change of a subscription is not a sale. It reaches KeyGrant through the subscription's own events (see Subscriptions).
  • The key email goes to the email on the Paddle customer, when the product's License delivery tab is set to KeyGrant sends them (see Key delivery email).
  • How many keys a sale creates, upgrades, timed models and held sales work as for Stripe (see How many keys and Held sales).

Settings on a price

Mapping a model to its price needs no custom data. To set more, put it in the price's custom data in Paddle (Catalog > Prices, or the Paddle API), where only you can. KeyGrant reads the custom data of the price on the transaction's first item, as it reads a Stripe Payment Link's metadata:

A price's custom data
{ "product": "my-product", "modelId": "mod_…" }

The fields are the ones on the Stripe page's Checkout metadata.

Subscriptions

A subscription key is valid until the end of the billing period Paddle reports, and its expiry moves forward with each renewal. The Grace after expiry, days the model had at the time of the sale applies to a failed renewal and a cancellation alike, as on the Stripe page.

EventWhat the key does
A renewalRuns to the end of the new billing period.
A failed renewal Paddle marks the subscription past_due. The key keeps working until the end of the last period paid for, plus its grace period.
A cancellation Keeps working until the date Paddle cancels the subscription, plus its grace period. Cancel at the end of the billing period in Paddle and that is the end of the period paid for.

Refunds and disputes

A refund in Paddle is an adjustment, and KeyGrant acts on it once Paddle approves it. A full refund takes back what its payment bought while Refunds revoke keys is on in Settings > Payments, as the table on the Stripe page says. A partial refund revokes nothing.

  • Full: a refund of the whole transaction, or refunds the transaction has had approved that now add up to all of it.
  • Waiting for approval: changes nothing yet.
  • Rejected: no money moved, so it changes nothing. One that KeyGrant asked for on a held sale holds the sale again (see Held sales).
  • A chargeback Paddle takes works as a lost dispute does for Stripe: it takes back what the payment bought, whatever Refunds revoke keys says.
  • A chargeback Paddle reverses puts back only what the chargeback took: the keys it revoked, the upgrades it took back, and the held sales it refunded. What an earlier refund of the same payment took stays taken.

A revoked key stops at the app's next check-in (see When a revoke takes effect).

Held sales

A held sale is a paid upgrade, or a timed model's purchase, that KeyGrant could not place on a key. Resolve it on Held sales, as for Stripe (see Resolve a held sale), with these differences:

  • Refund asks Paddle for an adjustment on the transaction, for what the sale charged for the upgrade. Paddle may hold it for its approval. The sale shows refunded at once.
  • If Paddle rejects the refund, the sale is held again and the next Refund asks Paddle afresh. KeyGrant keeps a record of each refund it asked for, so pressing Refund again while one waits asks Paddle for nothing more.
  • When KeyGrant cannot refund it, refund the transaction in Paddle, then choose Mark refunded. The dialog links to the transaction in your Paddle dashboard.

Test in the sandbox

A checkout in your Paddle sandbox, on the sandbox you linked, creates a test license, so you can rehearse a sale end to end (see Testing your integration).

  1. Link a sandbox key under Settings > Payments.
  2. Make the sandbox price. Its ID differs from the live price's, and a model maps to one. Give the sandbox price the custom data { "product": "<your product's API ID>", "modelId": "<the model's ID>" } (see Settings on a price) and the checkout creates a key of that model with no remapping.
  3. Run your checkout page against the sandbox, with Paddle.Environment.set('sandbox').

When Paddle refuses the key

If Paddle refuses the key KeyGrant holds, because it expired, was revoked or lacks a permission, a sale still creates its key from what Paddle sends. KeyGrant cannot read the buyer's email without the key, so the key email does not reach the customer, and it cannot check refunds and renewals with Paddle. Your account's owners are emailed, at most once a day for each link, until you paste a new key under Settings > Payments.

Unlink in Settings > Payments lets one environment's link go, after a confirmation.

  • New sales through it stop creating keys.
  • Nothing is deleted in Paddle. KeyGrant keeps listening for refunds, renewals and cancellations of keys already sold through it, and the card lists the links it still listens to.
  • A live sale made after you unlink is refused, so Paddle retries it for about 2 days, and KeyGrant creates the key if you link the account again in that time. KeyGrant emails your account's owners about such a sale at most once a day.
  • To sell through Stripe instead, unlink both environments, then connect Stripe.

Stop selling

  • Archiving a model or a product offers to archive its prices in Paddle: no new checkout can use an archived price, and subscriptions already on it keep renewing. A price KeyGrant archives for you is archived in the Paddle environment that has it.
  • Keys of an archived product cannot activate on a new device, so a key sold on one of its prices after it is archived cannot activate at all.
  • Unmapping a model leaves Paddle selling its price, and a paid checkout on that price then creates no key unless the price's custom data names the product.

Warning

Deleting a product cannot be undone, and its Paddle prices are not touched: a paid checkout on one then takes payment and creates no key. Archive them in Paddle first.