Paddle Discount Coupons API: Developer Guide
Complete developer guide to the Paddle Discounts API — create, apply, and manage percentage coupons programmatically. Includes cURL and Node.js examples for per-request PPP pricing.
Paddle's Discounts API lets you create, manage, and apply percentage or flat-amount coupons programmatically. This guide covers every endpoint you need, working code examples in cURL and Node.js, and the checkout integration pattern — plus how to automate coupon creation per request for dynamic regional pricing.
Prerequisites
- A Paddle Billing account (v2 API — not Paddle Classic)
- An API key from Paddle Dashboard → Developer Tools → Authentication
- Node.js 18+ for the JS examples, or any HTTP client for the cURL examples
All examples use the sandbox endpoint (sandbox-api.paddle.com). Swap it for api.paddle.com in production.
The Paddle Discounts API at a glance
| Method | Endpoint | What it does |
|---|---|---|
| POST | /discounts | Create a new discount |
| GET | /discounts | List all discounts (paginated) |
| GET | /discounts/{id} | Fetch a single discount by ID |
| PATCH | /discounts/{id} | Update description, usage limit, expiry |
| DELETE | /discounts/{id} | Archive a discount (non-reversible) |
Auth is a Bearer token on every request: Authorization: Bearer YOUR_PADDLE_API_KEY
Create a discount — cURL
The minimum payload requires type and amount. Set enabled_for_checkout: true so the coupon works in Paddle.js.
curl -X POST https://sandbox-api.paddle.com/discounts \
-H "Authorization: Bearer pdl_sdbx_apikey_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "percentage",
"amount": "40",
"description": "LATAM PPP discount – 40%",
"enabled_for_checkout": true,
"usage_limit": 1,
"maximum_recurring_intervals": 1
}'Successful response (HTTP 201):
{
"data": {
"id": "dsc_01h1vjes1y163xfej1py4dn23n",
"status": "active",
"type": "percentage",
"amount": "40",
"code": "ZXKH7P3M",
"description": "LATAM PPP discount – 40%",
"enabled_for_checkout": true,
"usage_limit": 1,
"times_used": 0,
"maximum_recurring_intervals": 1,
"expires_at": null,
"created_at": "2026-05-06T14:22:01.000Z",
"updated_at": "2026-05-06T14:22:01.000Z"
}
}The code field is auto-generated when you omit it. You can also specify your own: "code": "INDIA60". Codes must be unique across your account.
Key fields explained
| Field | Type | Notes |
|---|---|---|
| type | string | percentage, flat, or flat_per_seat |
| amount | string | Percentage value ("40") or minor units for flat ("1000" = $10.00) |
| currency_code | string | Required for flat type. ISO 4217 (e.g. "USD") |
| usage_limit | integer | null | Max total redemptions. null = unlimited |
| maximum_recurring_intervals | integer | null | How many billing cycles the discount applies. 1 = first payment only |
| expires_at | RFC 3339 | null | Hard expiry date. null = never expires |
| restrict_to | string[] | Array of price IDs — limits discount to specific products |
Create a discount — Node.js
Using the official @paddle/paddle-node-sdk package:
import { Paddle, Environment } from '@paddle/paddle-node-sdk'
const paddle = new Paddle('YOUR_PADDLE_API_KEY', {
environment: Environment.Sandbox,
})
async function createPPPDiscount(percent: number, label: string) {
const discount = await paddle.discounts.create({
type: 'percentage',
amount: String(percent),
description: label,
enabledForCheckout: true,
usageLimit: 1,
maximumRecurringIntervals: 1,
})
return discount // { id: 'dsc_xxx', code: 'ABCD1234', ... }
}
// Usage
const discount = await createPPPDiscount(40, 'Brazil – PPP 40%')
console.log(discount.id) // dsc_01h1vjes1y163xfej1py4dn23n
console.log(discount.code) // ZXKH7P3MApply a discount at checkout
Once you have a discount ID or code, pass it to Paddle.Checkout.open(). You can use either the discountId or the discountCode — both work.
// Using the discount ID (preferred for programmatic flows)
Paddle.Checkout.open({
items: [{ priceId: 'pri_01gsz8x8sawmvhz1pv30nge1ke', quantity: 1 }],
discountId: 'dsc_01h1vjes1y163xfej1py4dn23n',
customer: { email: user.email },
})
// Using the discount code (simpler if you set a human-readable code)
Paddle.Checkout.open({
items: [{ priceId: 'pri_01gsz8x8sawmvhz1pv30nge1ke', quantity: 1 }],
discountCode: 'BRAZIL40',
})Prefer discountId in programmatic flows — codes are case-sensitive and can collide if you generate many of them. IDs are stable and guaranteed unique.
List and filter discounts
// Fetch the first page of active discounts
const { data } = await paddle.discounts.list({ status: 'active' })
for (const d of data) {
console.log(d.id, d.code, d.timesUsed, '/', d.usageLimit ?? '∞')
}The list endpoint supports these query params:
status—active|archived|expiredcode— filter by exact code stringafter— cursor-based paginationper_page— results per page (max 200)
Update and archive discounts
// Extend the usage limit of an existing discount
await paddle.discounts.update('dsc_01h1vjes1y163xfej1py4dn23n', {
usageLimit: 50,
expiresAt: '2026-12-31T23:59:59Z',
})
// Archive (soft-delete) a discount — cannot be undone
await paddle.discounts.archive('dsc_01h1vjes1y163xfej1py4dn23n')Building per-request PPP coupons
A common pattern for regional pricing is: when a visitor loads your pricing page, your backend checks their country, computes the right discount percentage, creates a single-use Paddle coupon, and passes the coupon ID to the frontend for checkout injection.
Here is a minimal Express endpoint that does exactly that:
import express from 'express'
import { Paddle, Environment } from '@paddle/paddle-node-sdk'
import { getCountryFromIp } from './geo' // your geo lookup
import { getPPPDiscount } from './ppp-rules' // your discount config
const paddle = new Paddle(process.env.PADDLE_API_KEY!, {
environment: Environment.Production,
})
const app = express()
app.get('/api/checkout-discount', async (req, res) => {
const ip = req.ip
const country = await getCountryFromIp(ip) // e.g. "IN"
const percent = getPPPDiscount(country) // e.g. 60
if (!percent) {
return res.json({ discountId: null }) // no discount for this region
}
try {
const discount = await paddle.discounts.create({
type: 'percentage',
amount: String(percent),
description: `PPP-${country}-${percent}pct`,
enabledForCheckout: true,
usageLimit: 1, // single-use prevents sharing
maximumRecurringIntervals: 1, // first payment only
expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(), // 15 min TTL
})
res.json({ discountId: discount.id, percent })
} catch (err) {
console.error('Paddle coupon error:', err)
res.status(500).json({ discountId: null })
}
})
app.listen(3001)Frontend consuming the endpoint:
async function openCheckout(priceId: string) {
const { discountId } = await fetch('/api/checkout-discount').then(r => r.json())
Paddle.Checkout.open({
items: [{ priceId, quantity: 1 }],
...(discountId ? { discountId } : {}),
})
}Why this gets hard at scale
The pattern above works, but creates real operational overhead as you grow:
- Rate limits — Paddle's API allows ~100 discount creates/minute. High-traffic pricing pages will hit this quickly.
- Coupon accumulation — every pageview creates a coupon. You'll have tens of thousands of single-use coupons with no automatic cleanup.
- Geo reliability — maintaining your own IP-to-country lookup adds a dependency that needs to be fast (<30ms) and accurate for VPN detection.
- Cold starts — serverless functions add latency before the coupon ID reaches the frontend, delaying checkout open.
- Caching — you can't naively cache coupon IDs (they're single-use), so every request creates a round-trip to Paddle.
The PriceParity approach
PriceParity is built specifically to solve this. It runs the entire flow — geo lookup → PPP rule match → Paddle coupon creation → checkout injection — from a single SDK script tag. The engine endpoint handles caching, rate-limit back-pressure, and VPN detection transparently.
If you prefer to keep control of the checkout flow and just want the coupon ID from an API call, you can call the engine endpoint directly:
// Server-side: fetch a discount coupon for the visitor's IP
const response = await fetch('https://api.priceparity.net/v1/engine/evaluate', {
method: 'POST',
headers: {
'X-API-Key': process.env.PRICEPARITY_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId: 'pri_01gsz8x8sawmvhz1pv30nge1ke',
visitorIp: req.ip,
}),
})
const { discountPercent, paddleCouponCode, localizedPrice } = await response.json()
// paddleCouponCode is a ready-to-use single-use Paddle coupon
// Pass it to the frontend and open checkout with discountCode: paddleCouponCodeThis gives you the coupon code without managing Paddle API rate limits, geo lookups, or coupon lifecycle yourself. Full API reference →
Handling errors gracefully
Always have a fallback. If coupon creation fails — Paddle API is down, rate limit hit, invalid price ID — the checkout should still open at full price rather than breaking:
async function openCheckoutWithFallback(priceId: string) {
let discountId: string | null = null
try {
const res = await fetch('/api/checkout-discount', { signal: AbortSignal.timeout(3000) })
if (res.ok) discountId = (await res.json()).discountId
} catch {
// Swallow — open checkout at full price
}
Paddle.Checkout.open({
items: [{ priceId, quantity: 1 }],
...(discountId ? { discountId } : {}),
})
}Restricting a discount to specific products
Use restrictTo to prevent a coupon from being applied to the wrong price:
await paddle.discounts.create({
type: 'percentage',
amount: '40',
description: 'LATAM – Pro plan only',
enabledForCheckout: true,
usageLimit: 1,
restrictTo: ['pri_01gsz8x8sawmvhz1pv30nge1ke'], // Pro plan price ID
})Flat discounts vs percentage discounts
For PPP pricing, percentage discounts are almost always the right choice. Flat discounts (type: "flat") require a currency_code and only apply in that currency — they don't work when Paddle localises the price to a different currency. Percentage discounts work across all currencies automatically.
Summary
- Use
POST /discountswithtype: "percentage"andusage_limit: 1for per-session PPP coupons - Pass the resulting
idtoPaddle.Checkout.open()asdiscountId - Set
maximum_recurring_intervals: 1so the discount only applies to the first payment - Set
expires_atto a short TTL (10–15 min) to keep your discount list clean - Always implement a fallback — open at full price if coupon creation fails
- Use
restrictToto limit coupons to specific price IDs
Want all of this handled automatically — geo, coupons, and checkout injection?
Try PriceParity free — live in 5 minutes →Ready to add PPP pricing to your Paddle checkout?
Live in 5 minutes. No backend changes. 14-day free trial.
Start free trial →