Summary and recommendation
Paddle Billing's REST API (base URL: https://api.paddle.com) authenticates via Bearer token using server-side API keys - there is no OAuth 2.0 flow for server-to-server calls. Keys are either default (full access) or restricted (resource-scoped, configured in the dashboard).
A critical distinction: Paddle Classic and Paddle Billing are separate products with incompatible endpoints, auth models, and object schemas; everything here applies to Paddle Billing only. Sandbox and production environments use separate keys and separate base URLs (https://sandbox-api.paddle.com vs https://api.paddle.com) - keys are not interchangeable.
Rate limits are enforced at 100 requests/second per API key; HTTP 429 is returned on breach, and Paddle recommends exponential backoff.
API quick reference
| Has user API | Yes |
| Auth method | API Key (Bearer token) – Paddle uses server-side API keys passed as Bearer tokens in the Authorization header. No OAuth 2.0 flow is documented for server-to-server calls. |
| Base URL | Official docs |
| SCIM available | No |
Authentication
Auth method: API Key (Bearer token) – Paddle uses server-side API keys passed as Bearer tokens in the Authorization header. No OAuth 2.0 flow is documented for server-to-server calls.
Setup steps
- Log in to the Paddle dashboard (sandbox: sandbox-vendors.paddle.com; production: vendors.paddle.com).
- Navigate to Developer Tools > Authentication.
- Generate an API key (default key or a named key with restricted permissions).
- Store the key securely; it is shown only once.
- Include the key in every request: Authorization: Bearer {API_KEY}.
- For sandbox testing, use base URL https://sandbox-api.paddle.com and a sandbox API key.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| No OAuth scopes | Paddle API keys are not scope-based in the OAuth sense. Access is controlled by key type: default keys have full access; restricted keys can be limited to specific resources via the dashboard. | All API operations |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Unique Paddle-generated customer ID (prefix: ctm_). | auto-generated | immutable | Used as path parameter in all customer endpoints. |
| string | Customer email address. | required | optional | Must be a valid email format. | |
| name | string | Full name of the customer. | optional | optional | |
| locale | string | BCP 47 locale string (e.g., en-US) used for localized communications. | optional | optional | |
| marketing_consent | boolean | Whether the customer has consented to marketing communications. | optional | optional | |
| status | enum | Customer status: active or archived. | auto-set to active | optional (archive via dedicated endpoint) | Archived customers cannot transact. |
| custom_data | object | Arbitrary key-value pairs for storing custom metadata. | optional | optional | Values must be strings. Max 50 keys. |
| created_at | string (RFC 3339) | Timestamp when the customer was created. | auto-generated | immutable | |
| updated_at | string (RFC 3339) | Timestamp of the last update. | auto-generated | auto-updated | |
| import_meta | object | Metadata about the customer if imported from another system (external_id, imported_from). | optional | immutable after set | Useful for mapping Paddle customers to records in external systems. |
Core endpoints
List customers
- Method: GET
- URL:
https://api.paddle.com/customers - Watch out for: Pagination is cursor-based; use meta.pagination.next as the
afterparam. Filtering by email requires exact match.
Request example
GET /customers?per_page=50&status=active
Authorization: Bearer {API_KEY}
Response example
{
"data": [{"id":"ctm_01h...","email":"user@example.com","name":"Jane Doe","status":"active"}],
"meta": {"pagination":{"per_page":50,"has_more":true,"next":"ctm_01h..."}}
}
Get customer
- Method: GET
- URL:
https://api.paddle.com/customers/{customer_id} - Watch out for: Returns 404 if the customer_id does not exist in the environment (sandbox vs. production keys are separate).
Request example
GET /customers/ctm_01h...
Authorization: Bearer {API_KEY}
Response example
{
"data": {"id":"ctm_01h...","email":"user@example.com","name":"Jane Doe","status":"active","custom_data":{}}
}
Create customer
- Method: POST
- URL:
https://api.paddle.com/customers - Watch out for: Duplicate email addresses are allowed; Paddle does not enforce email uniqueness across customers.
Request example
POST /customers
Authorization: Bearer {API_KEY}
Content-Type: application/json
{"email":"user@example.com","name":"Jane Doe","locale":"en-US"}
Response example
{
"data": {"id":"ctm_01h...","email":"user@example.com","name":"Jane Doe","status":"active","created_at":"2024-01-01T00:00:00Z"}
}
Update customer
- Method: PATCH
- URL:
https://api.paddle.com/customers/{customer_id} - Watch out for: PATCH is partial; only supplied fields are updated. Setting custom_data to {} clears all custom metadata.
Request example
PATCH /customers/ctm_01h...
Authorization: Bearer {API_KEY}
Content-Type: application/json
{"name":"Jane Smith","custom_data":{"plan":"pro"}}
Response example
{
"data": {"id":"ctm_01h...","email":"user@example.com","name":"Jane Smith","status":"active"}
}
List addresses for customer
- Method: GET
- URL:
https://api.paddle.com/customers/{customer_id}/addresses - Watch out for: Addresses are sub-resources; they must be managed separately from the customer object.
Request example
GET /customers/ctm_01h.../addresses
Authorization: Bearer {API_KEY}
Response example
{
"data": [{"id":"add_01h...","country_code":"US","postal_code":"10001","status":"active"}]
}
Create address for customer
- Method: POST
- URL:
https://api.paddle.com/customers/{customer_id}/addresses - Watch out for: country_code is required and must be a valid ISO 3166-1 alpha-2 code.
Request example
POST /customers/ctm_01h.../addresses
Authorization: Bearer {API_KEY}
Content-Type: application/json
{"country_code":"US","postal_code":"10001"}
Response example
{
"data": {"id":"add_01h...","country_code":"US","postal_code":"10001","status":"active"}
}
List subscriptions for customer
- Method: GET
- URL:
https://api.paddle.com/subscriptions?customer_id={customer_id} - Watch out for: Subscriptions are not nested under /customers; filter via query param on the /subscriptions endpoint.
Request example
GET /subscriptions?customer_id=ctm_01h...
Authorization: Bearer {API_KEY}
Response example
{
"data": [{"id":"sub_01h...","status":"active","customer_id":"ctm_01h...","current_billing_period":{}}]
}
Get customer portal session (magic link)
- Method: POST
- URL:
https://api.paddle.com/customers/{customer_id}/portal-sessions - Watch out for: Portal session URLs are single-use and expire. Do not cache or reuse them.
Request example
POST /customers/ctm_01h.../portal-sessions
Authorization: Bearer {API_KEY}
Content-Type: application/json
{"subscription_ids":["sub_01h..."]}
Response example
{
"data": {"id":"cpls_01h...","customer_id":"ctm_01h...","urls":{"general":{"overview":"https://customer-portal.paddle.com/..."}},"created_at":"2024-01-01T00:00:00Z"}
}
Rate limits, pagination, and events
- Rate limits: Paddle enforces rate limits per API key. The documented limit is 100 requests per second per API key for the Paddle Billing API. Exceeding the limit returns HTTP 429.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: HTTP 429 is returned when the limit is exceeded. Paddle recommends exponential backoff. Specific header names (e.g., X-RateLimit-Limit, Retry-After) are referenced in the API overview but exact header names should be confirmed against live responses.
- Pagination method: cursor
- Default page size: 50
- Max page size: 200
- Pagination pointer: after (cursor-based; use the
has_moreflag andmeta.pagination.nextcursor from the response)
| Plan | Limit | Concurrent |
|---|---|---|
| All plans (pay-as-you-go) | 100 requests/second per API key | 0 |
- Webhooks available: Yes
- Webhook notes: Paddle sends signed HTTP POST webhooks to a configured endpoint for all major billing and customer lifecycle events. Webhook payloads are signed with HMAC-SHA256 using a secret key configurable in the dashboard.
- Alternative event strategy: Polling the /customers and /subscriptions endpoints with updated_at filters can substitute for webhooks in low-volume scenarios.
- Webhook events: customer.created, customer.updated, subscription.created, subscription.updated, subscription.canceled, subscription.paused, subscription.resumed, subscription.past_due, transaction.created, transaction.updated, transaction.completed, transaction.payment_failed, address.created, address.updated
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Not documented
- Endpoint: Not documented
Limitations:
- Paddle does not offer a SCIM 2.0 API. User/customer provisioning must be done via the Paddle REST API directly.
- No public documentation for SSO or SCIM integration exists as of the policy date.
Common scenarios
The primary integration pattern for identity graph maintenance is mapping Paddle's ctm_-prefixed customer ID to your internal user record at creation time.
POST /customers to create a billing contact, store the returned ctm_ ID against your user record, and use it as the stable join key for all downstream subscription and transaction calls.
Paddle does not enforce email uniqueness - multiple customer records can share the same email address - so deduplication must be owned by the caller before issuing a POST.
For profile sync, use PATCH /customers/{customer_id} with only changed fields; sending custom_data: {} in any PATCH payload will silently clear all existing custom metadata. For subscription lifecycle sync, configure signed webhooks (HMAC-SHA256 via the Paddle-Signature header) and listen for customer.
updated, subscription. created, subscription.
updated, and subscription. canceled; implement idempotency on the event id field to handle Paddle's exponential-backoff retry behavior.
Pagination across /customers and /subscriptions is cursor-based: use meta. pagination.
next as the after parameter, with a default page size of 50 and a maximum of 200.
Provision a new customer on signup
- POST /customers with email, name, and any custom_data (e.g., internal user ID).
- Store the returned ctm_ ID in your application database mapped to the internal user record.
- Optionally POST /customers/{customer_id}/addresses if billing address is collected at signup.
- Use the ctm_ ID in subsequent transaction or subscription creation calls.
Watch out for: Paddle does not enforce email uniqueness. Always check your own database for an existing ctm_ ID before creating a new customer to avoid duplicates.
Update customer profile on user account change
- Retrieve the stored ctm_ ID for the user from your database.
- PATCH /customers/{customer_id} with only the changed fields (name, locale, custom_data, etc.).
- Handle 404 gracefully in case the customer was archived or the ID is stale.
Watch out for: Sending custom_data: {} in a PATCH will clear all existing custom metadata. Only include custom_data in the payload if you intend to update it.
Sync customer subscription status to your application
- Configure a webhook endpoint in the Paddle dashboard and note the webhook secret.
- Listen for customer.updated, subscription.created, subscription.updated, and subscription.canceled events.
- Verify the Paddle-Signature header on each inbound webhook using HMAC-SHA256 and your webhook secret.
- Extract customer_id and subscription status from the payload and update your application database.
- Return HTTP 200 promptly; process asynchronously if needed to avoid timeout-triggered retries.
Watch out for: Paddle retries failed webhook deliveries with exponential backoff. Implement idempotency using the event id field to avoid processing duplicate events.
Why building this yourself is a trap
There is no SCIM 2.0 API and no public SSO documentation for Paddle - provisioning and deprovisioning of dashboard team members has no programmatic path. The Paddle API manages billing contacts (customers), not application-level user accounts; there is no concept of user passwords, sessions, or application roles in the API surface.
This distinction matters for identity graph integrations: ctm_ records represent payers, not authenticated users, and the two must be explicitly joined in your own data layer. custom_data fields accept only string values - nested objects will return a validation error.
Portal session URLs (magic links, generated via POST /customers/{customer_id}/portal-sessions) are single-use and short-lived; they must be generated on demand and never cached.
For teams building on top of Paddle through an MCP server with 60+ deep IT/identity integrations, the absence of SCIM means any identity lifecycle automation must be constructed entirely against the REST API with caller-managed deduplication and no standardized provisioning contract.
Automate Paddle workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.