All posts
Tutorial8 min read

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.

PriceParity TeamMay 6, 2026

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

MethodEndpointWhat it does
POST/discountsCreate a new discount
GET/discountsList 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

FieldTypeNotes
typestringpercentage, flat, or flat_per_seat
amountstringPercentage value ("40") or minor units for flat ("1000" = $10.00)
currency_codestringRequired for flat type. ISO 4217 (e.g. "USD")
usage_limitinteger | nullMax total redemptions. null = unlimited
maximum_recurring_intervalsinteger | nullHow many billing cycles the discount applies. 1 = first payment only
expires_atRFC 3339 | nullHard expiry date. null = never expires
restrict_tostring[]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) // ZXKH7P3M

Apply 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:

  • statusactive | archived | expired
  • code — filter by exact code string
  • after — cursor-based pagination
  • per_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: paddleCouponCode

This 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 /discounts with type: "percentage" and usage_limit: 1 for per-session PPP coupons
  • Pass the resulting id to Paddle.Checkout.open() as discountId
  • Set maximum_recurring_intervals: 1 so the discount only applies to the first payment
  • Set expires_at to 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 restrictTo to 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 →