Payments

On this page

Paddle-compatible Billing API

A mock of Paddle Billing's server API with sandbox semantics: products, prices, customers, transactions, subscriptions and refunds, Paddle's response and error envelopes, a hosted test checkout and signed webhooks. Only test card numbers are accepted at the checkout; no real money moves.

Testlie is not affiliated with, endorsed or sponsored by Paddle. "Paddle" is used only to describe API compatibility.

Connect

Create a Paddle-compatible gateway under Payment gateways. Use its base URL in place of https://sandbox-api.paddle.com and send the mock's API key as Authorization: Bearer <API key>.

Credential Use
API key (pdl_sdbx_apikey_...) Bearer auth on every call
Notification secret key (pdl_ntfset_...) Verifies Paddle-Signature on webhooks
Client-side token (test_...) Refused by the server API, as Paddle does; Paddle.js is not mocked

Responses use Paddle's envelope, { "data": ..., "meta": { "request_id": "..." } }, with meta.pagination (per_page, next, has_more, estimated_total) on lists. Page with ?per_page= (up to 200) and the after cursor in next. Ids are Paddle-shaped: pro_, pri_, ctm_, txn_, txnitm_, sub_, adj_.

Errors use Paddle's shape:

json
{ "error": { "type": "request_error", "code": "bad_request", "detail": "Invalid request.", "documentation_url": "https://developer.paddle.com/v1/errors/shared/bad_request", "errors": [{ "field": "unit_price.amount", "message": "..." }] }, "meta": { "request_id": "..." } }

A missing key answers 401 authentication_missing; a malformed header 403 authentication_malformed; a wrong key 403 invalid_token. These are refused before the quota and do not count.

Endpoints

Endpoint
POST / GET /products, GET / PATCH /products/{id} name, tax_category, description, image_url, custom_data, status (active, archived)
POST / GET /prices, GET / PATCH /prices/{id} product_id, description, unit_price: { amount, currency_code } (amount a string in the lowest denomination, e.g. "1500"), optional billing_cycle: { interval, frequency }, quantity: { minimum, maximum }. Filters product_id, recurring, status
POST / GET /customers, GET / PATCH /customers/{id} email (unique: a second customer with it is 409 customer_already_exists), name, locale, custom_data. Filters email, search, status
POST /transactions items: [{ price_id, quantity }], optional customer_id, custom_data, checkout: { url }. ready with a customer, else draft
GET /transactions, GET / PATCH /transactions/{id} PATCH changes items, customer_id, custom_data, or sets status: "canceled", while draft or ready. Filters status, customer_id, subscription_id
GET /subscriptions, GET / PATCH /subscriptions/{id} PATCH takes custom_data and scheduled_change: null (removes a scheduled cancel or pause). Filters status, customer_id
POST /subscriptions/{id}/cancel effective_from: next_billing_period (default; sets scheduled_change) or immediately
POST /subscriptions/{id}/pause effective_from as above, optional resume_at
POST /subscriptions/{id}/resume Resumes a paused subscription with a new billing period, or drops a scheduled pause
POST / GET /adjustments Refunds: action: "refund", transaction_id, reason, and type: "full" or items: [{ item_id, type: "full" | "partial", amount }] where item_id is a details.line_items[].id. Filters transaction_id, action

Transactions carry details.totals (subtotal, tax, total, grand_total, and once paid fee and earnings), details.adjusted_totals after refunds, details.line_items and payments[]. The mock charges no tax (tax "0") and a fee of 5% + 50 in the lowest denomination.

Not implemented: Paddle.js and overlay or inline checkout, addresses and businesses, discounts, non-catalog prices, tax calculation, POST /transactions/preview, invoices (/transactions/{id}/invoice), manual collection and credits, trials, subscription renewals and item changes, one-time subscription charges, the events and notifications APIs, and simulations.

The test checkout

Every transaction's checkout.url opens a hosted test checkout (Paddle sets this to your default payment link with ?_ptxn=). It shows the items and total and asks for the buyer's email when the transaction has no customer. Pay with a test card:

  • The transaction becomes completed, with a captured payment (brand and last four digits only).
  • A customer is created (or matched by email) when the checkout collected one.
  • If any price is recurring, an active subscription starts with the next billing date one cycle away, and the transaction gets subscription_id and billing_period.
  • If you sent checkout: { url } when creating the transaction, the browser goes back there with ?_ptxn=<transaction id>.

A declined card adds a payment with status: "error" and Paddle's error_code, leaves the transaction ready and sends transaction.payment_failed; the buyer can try another card. "Close checkout" returns to your URL without paying.

Test cards only

A card number must pass the Luhn check and be one of these test numbers. Anything else, including a real card, is refused; card numbers and security codes are never stored.

Card Result
4242 4242 4242 4242, 4000 0566 5566 5556, 4111 1111 1111 1111 (Visa) Succeeds
5555 5555 5555 4444, 2223 0000 4841 0010 (Mastercard) Succeeds
3782 822463 10005 (Amex), 6011 1111 1111 1117 (Discover), 3566 0020 2036 0505 (JCB) Succeeds
4000 0038 0000 0446 3-D Secure, succeeds
4000 0000 0000 0002 declined
4000 0000 0000 9995 not_enough_balance
4000 0000 0000 0069 expired_card
4000 0084 0000 1629 authentication_failed
4100 0000 0000 0019 fraud

The Outcome scenario overrides the table: "succeed" approves every test card, "decline" declines with the chosen code (declined, not_enough_balance, expired_card, authentication_failed, fraud, blocked_card, issuer_unavailable, psp_error).

Refunds

POST /adjustments answers 201 with status: "pending_approval" and sends adjustment.created; like the sandbox, the refund is approved shortly after: reads show approved and adjustment.updated (status approved) follows about three seconds later. Refunds can't exceed what is left on each line item (400 bad_request); refunding a fully refunded transaction is adjustment_amount_above_remaining_allowed, and only completed transactions can be refunded.

Webhooks

Set the gateway's webhook URL to receive Paddle notifications:

json
{ "event_id": "evt_...", "event_type": "transaction.completed", "occurred_at": "2026-10-30T09:00:00.000Z", "notification_id": "ntf_...", "data": { "id": "txn_...", "status": "completed", ... } }

Events: transaction.created, transaction.ready, transaction.updated, transaction.paid, transaction.completed, transaction.payment_failed, transaction.canceled, subscription.created, subscription.activated, subscription.updated, subscription.paused, subscription.resumed, subscription.canceled, customer.created, customer.updated, product.created, product.updated, price.created, price.updated, adjustment.created, adjustment.updated.

Each delivery has a Paddle-Signature: ts=<unix seconds>;h1=<signature> header, where the signature is the hex HMAC-SHA256 of <ts>:<raw body> with the notification secret key. Paddle's SDKs verify it (paddle.webhooks.unmarshal(rawBody, secret, signature) in Node). The webhook scenarios (delay, drop, duplicate, bad signature) and Next time webhook outcomes apply, and failed deliveries are retried.