Ledger

HTTP API

REST API reference for all Ledger billing endpoints.

All Ledger HTTP endpoints are served under the /ledger prefix and return JSON. Webhook endpoints are served under /webhooks. Authentication and tenant resolution depend on your middleware configuration (see Multi-tenancy).

Authentication

Ledger does not enforce a specific authentication scheme. Authentication is handled at the middleware layer before requests reach Ledger routes. Common patterns:

  • Bearer token -- Pass Authorization: Bearer <token> header. Your middleware validates the token and injects tenant/app context.
  • API key -- Pass X-API-Key: <key> header. Middleware resolves the key to a tenant.
  • Forge scope -- When running inside the Forgery ecosystem, Forge automatically extracts tenant and app identifiers from the authenticated scope.

All requests must carry tenant context. Requests without valid tenant context receive 401 Unauthorized.

Pagination

List endpoints support cursor-based pagination with limit and offset query parameters.

ParameterTypeDefaultDescription
limitint20Maximum number of results to return (max: 100)
offsetint0Number of results to skip

Response envelope for list endpoints:

{
  "data": [],
  "pagination": {
    "total": 150,
    "limit": 20,
    "offset": 0,
    "has_more": true
  }
}

Error response format

All error responses follow a consistent JSON structure:

{
  "error": {
    "code": "subscription_not_found",
    "message": "Subscription sub_01h2xcejqtf2nbrexx3vqjhp44 does not exist",
    "type": "not_found",
    "param": "subscription_id",
    "details": {}
  }
}
FieldDescription
codeMachine-readable error code (snake_case)
messageHuman-readable error message
typeError category: validation, not_found, conflict, payment, provider, server
paramThe parameter that caused the error (optional)
detailsAdditional context (optional)

HTTP status code mapping:

HTTP statusCondition
400 Bad RequestInvalid input, missing required fields, validation errors
401 UnauthorizedMissing or invalid authentication
402 Payment RequiredPayment failed, card declined
403 ForbiddenFeature not enabled, quota exceeded
404 Not FoundResource does not exist for the current tenant
409 ConflictDuplicate resource (plan name, email, idempotency key)
500 Internal Server ErrorStore failure, provider API error
504 Gateway TimeoutPayment provider timeout

See Error Handling for the full error reference.


Plans

Plans define available features, usage limits, and pricing tiers. See Plans for details.

POST /ledger/plans

Create a new billing plan.

Request body:

{
  "name": "Pro",
  "slug": "pro",
  "description": "Professional plan with advanced features",
  "currency": "usd",
  "status": "active",
  "trial_days": 14,
  "features": [
    {
      "key": "api_calls",
      "name": "API Calls",
      "type": "metered",
      "limit": 100000,
      "period": "monthly",
      "soft_limit": false
    },
    {
      "key": "seats",
      "name": "Team Seats",
      "type": "seat",
      "limit": 10,
      "period": "none"
    },
    {
      "key": "sso",
      "name": "Single Sign-On",
      "type": "boolean",
      "limit": 1,
      "period": "none"
    }
  ],
  "pricing": {
    "base_amount": { "amount": 4999, "currency": "usd" },
    "billing_period": "monthly",
    "tiers": [
      {
        "feature_key": "api_calls",
        "type": "graduated",
        "up_to": 100000,
        "unit_amount": { "amount": 0, "currency": "usd" },
        "flat_amount": { "amount": 0, "currency": "usd" }
      },
      {
        "feature_key": "api_calls",
        "type": "graduated",
        "up_to": -1,
        "unit_amount": { "amount": 1, "currency": "usd" },
        "flat_amount": { "amount": 0, "currency": "usd" }
      }
    ]
  },
  "metadata": { "tier": "professional" }
}

Response 201 Created

{
  "id": "plan_01hqx3qfz5ekth2h5y6z6r099x",
  "name": "Pro",
  "slug": "pro",
  "description": "Professional plan with advanced features",
  "currency": "usd",
  "status": "active",
  "trial_days": 14,
  "features": [...],
  "pricing": {...},
  "app_id": "myapp",
  "metadata": { "tier": "professional" },
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}

Errors:

StatusCodeCondition
400invalid_inputMissing required fields (name, currency)
400duplicate_featureDuplicate feature key in features array
400invalid_pricingInvalid pricing configuration
409already_existsPlan with this slug already exists

GET /ledger/plans

List plans for the current app.

Query parameters:

ParameterTypeDescription
statusstringFilter by status: active, archived, draft
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK

{
  "data": [
    {
      "id": "plan_01hqx3qfz5ekth2h5y6z6r099x",
      "name": "Pro",
      "slug": "pro",
      "status": "active",
      "currency": "usd",
      "features": [...],
      "pricing": {...},
      "created_at": "2025-01-15T10:30:00Z",
      "updated_at": "2025-01-15T10:30:00Z"
    }
  ],
  "pagination": { "total": 3, "limit": 20, "offset": 0, "has_more": false }
}

GET /ledger/plans/{name}

Get a plan by slug name.

Response 200 OK -- Plan object.

Errors: 404 Not Found if plan does not exist.


PUT /ledger/plans/{name}

Update a plan by slug name. Only provided fields are updated.

Request body:

{
  "description": "Updated description",
  "trial_days": 30,
  "features": [...],
  "pricing": {...},
  "metadata": { "tier": "professional", "version": "2" }
}

Response 200 OK -- Updated Plan object.

Errors:

StatusCodeCondition
400invalid_inputInvalid field values
400plan_archivedCannot update an archived plan
404plan_not_foundPlan does not exist

DELETE /ledger/plans/{id}

Delete a plan by ID. Plans with active subscriptions cannot be deleted; archive them instead.

Response 204 No Content

Errors:

StatusCodeCondition
400plan_in_usePlan has active subscriptions
404plan_not_foundPlan does not exist

Subscriptions

Subscriptions connect tenants to plans and track billing periods. See Subscriptions for details.

POST /ledger/subscriptions

Create a new subscription.

Request body:

{
  "tenant_id": "acme-corp",
  "plan_id": "plan_01hqx3qfz5ekth2h5y6z6r099x",
  "status": "active",
  "metadata": { "source": "self-serve" }
}

Response 201 Created

{
  "id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3",
  "tenant_id": "acme-corp",
  "plan_id": "plan_01hqx3qfz5ekth2h5y6z6r099x",
  "status": "active",
  "current_period_start": "2025-01-15T10:30:00Z",
  "current_period_end": "2025-02-15T10:30:00Z",
  "app_id": "myapp",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}

Errors:

StatusCodeCondition
400invalid_inputMissing tenant_id or plan_id
404plan_not_foundReferenced plan does not exist
409subscription_existsTenant already has an active subscription

GET /ledger/subscriptions

List subscriptions. Filtered by current tenant context.

Query parameters:

ParameterTypeDescription
tenant_idstringFilter by tenant ID
statusstringFilter by status: active, trialing, past_due, canceled, expired, paused
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of Subscription objects.


GET /ledger/subscriptions/{id}

Get a subscription by ID.

Response 200 OK -- Subscription object.

Errors: 404 Not Found if subscription does not exist.


PUT /ledger/subscriptions/{id}

Update a subscription. Use this for metadata changes; use dedicated action endpoints for status changes.

Request body:

{
  "metadata": { "support_tier": "premium" }
}

Response 200 OK -- Updated Subscription object.


POST /ledger/subscriptions/{id}/cancel

Cancel a subscription. By default, cancels at the end of the current billing period. Set immediately to cancel right away.

Request body:

{
  "immediately": false
}

Response 200 OK

{
  "id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3",
  "status": "active",
  "cancel_at": "2025-02-15T10:30:00Z"
}

When immediately is true, the status changes to canceled and canceled_at is set to the current time. The entitlement cache is automatically invalidated.

Errors:

StatusCodeCondition
400subscription_canceledAlready canceled
404subscription_not_foundSubscription does not exist

POST /ledger/subscriptions/{id}/pause

Pause a subscription. Paused subscriptions retain their period but do not accrue usage or generate invoices.

Request body:

{
  "resume_at": "2025-03-01T00:00:00Z"
}

Response 200 OK -- Subscription object with status: "paused".

Errors:

StatusCodeCondition
400invalid_stateSubscription is not active
404subscription_not_foundSubscription does not exist

POST /ledger/subscriptions/{id}/resume

Resume a paused subscription.

Response 200 OK -- Subscription object with status: "active".

Errors:

StatusCodeCondition
400invalid_stateSubscription is not paused
404subscription_not_foundSubscription does not exist

POST /ledger/subscriptions/{id}/change-plan

Change the plan for an active subscription. Handles proration and entitlement cache invalidation.

Request body:

{
  "plan_id": "plan_01hqx3qfz5ekth2h5y6z6r099y",
  "prorate": true
}

Response 200 OK -- Updated Subscription object with the new plan.

Errors:

StatusCodeCondition
400invalid_upgradeInvalid plan transition
400invalid_downgradeInvalid plan downgrade
404subscription_not_foundSubscription does not exist
404plan_not_foundTarget plan does not exist

Metering

Track and query usage events for metered features. See Metering for details.

POST /ledger/meter/events

Ingest a single usage event. The event is buffered and flushed to the store asynchronously.

Request body:

{
  "feature_key": "api_calls",
  "quantity": 1,
  "idempotency_key": "req_abc123",
  "metadata": { "endpoint": "/v1/users" }
}

Response 202 Accepted

{
  "id": "uevt_01hqx3qh4r5kjyhwkrp5j4wsyb",
  "status": "buffered"
}

The 202 status indicates the event has been accepted into the buffer and will be persisted asynchronously. Tenant and app context are extracted from the request context.

Errors:

StatusCodeCondition
400invalid_inputMissing feature_key or invalid quantity
409duplicate_eventDuplicate idempotency key
429meter_buffer_fullBuffer is full, try again later

POST /ledger/meter/events/batch

Ingest multiple usage events in a single request.

Request body:

{
  "events": [
    { "feature_key": "api_calls", "quantity": 5, "idempotency_key": "batch_001" },
    { "feature_key": "storage_gb", "quantity": 2, "idempotency_key": "batch_002" }
  ]
}

Response 202 Accepted

{
  "accepted": 2,
  "rejected": 0,
  "results": [
    { "id": "uevt_01hqx3qh4r5kjyhwkrp5j4wsyb", "status": "buffered" },
    { "id": "uevt_01hqx3qh4r5kjyhwkrp5j4wsyc", "status": "buffered" }
  ]
}

GET /ledger/meter/usage/{subscriptionId}/{meter}

Get aggregated usage for a specific meter within the current billing period.

Response 200 OK

{
  "feature_key": "api_calls",
  "subscription_id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3",
  "current_usage": 45230,
  "limit": 100000,
  "remaining": 54770,
  "period_start": "2025-01-15T00:00:00Z",
  "period_end": "2025-02-15T00:00:00Z"
}

GET /ledger/meter/usage/{subscriptionId}/{meter}/history

Get historical usage for a meter across billing periods.

Query parameters:

ParameterTypeDescription
startdatetimeStart of date range (ISO 8601)
enddatetimeEnd of date range (ISO 8601)
granularitystringdaily, weekly, monthly

Response 200 OK

{
  "feature_key": "api_calls",
  "data": [
    { "period": "2025-01-01", "usage": 32100 },
    { "period": "2025-01-02", "usage": 28500 },
    { "period": "2025-01-03", "usage": 41200 }
  ]
}

GET /ledger/meter/events

Query raw usage events.

Query parameters:

ParameterTypeDescription
feature_keystringFilter by feature key
startdatetimeStart of date range (ISO 8601)
enddatetimeEnd of date range (ISO 8601)
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of UsageEvent objects.


Entitlements

Check feature access and quota for the current tenant. See Entitlements for details.

POST /ledger/entitlements/check

Check if the current tenant is entitled to use a feature. Returns sub-millisecond with cache hit.

Request body:

{
  "feature_key": "api_calls"
}

Response 200 OK

{
  "allowed": true,
  "feature": "api_calls",
  "used": 45230,
  "limit": 100000,
  "remaining": 54770,
  "soft_limit": false
}

When access is denied:

{
  "allowed": false,
  "feature": "api_calls",
  "used": 100500,
  "limit": 100000,
  "remaining": 0,
  "soft_limit": false,
  "reason": "quota exceeded"
}

Errors:

StatusCodeCondition
400invalid_inputMissing feature_key
403no_active_subscriptionNo active subscription for tenant

POST /ledger/entitlements/check-batch

Check multiple features in a single request.

Request body:

{
  "feature_keys": ["api_calls", "seats", "sso"]
}

Response 200 OK

{
  "results": {
    "api_calls": { "allowed": true, "used": 45230, "limit": 100000, "remaining": 54770 },
    "seats": { "allowed": true, "used": 3, "limit": 10, "remaining": 7 },
    "sso": { "allowed": true, "used": 0, "limit": 1, "remaining": 1 }
  }
}

GET /ledger/entitlements/{customerId}

Get all entitlements for a customer, including usage across all features in their current plan.

Response 200 OK

{
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "plan": "Pro",
  "plan_id": "plan_01hqx3qfz5ekth2h5y6z6r099x",
  "entitlements": [
    { "feature": "api_calls", "allowed": true, "used": 45230, "limit": 100000, "remaining": 54770, "type": "metered" },
    { "feature": "seats", "allowed": true, "used": 3, "limit": 10, "remaining": 7, "type": "seat" },
    { "feature": "sso", "allowed": true, "used": 0, "limit": 1, "remaining": 1, "type": "boolean" }
  ]
}

POST /ledger/entitlements/{customerId}/invalidate

Invalidate the entitlement cache for a customer. Use after manual plan changes or data corrections.

Response 204 No Content


Invoices

Generate, manage, and finalize invoices. See Invoicing for details.

POST /ledger/invoices

Generate an invoice for a subscription. Aggregates usage charges, applies base fees, calculates taxes, and creates line items.

Request body:

{
  "subscription_id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3"
}

Response 201 Created

{
  "id": "inv_01hqx3qgf2c8mhwy9s6zwmf3yh",
  "tenant_id": "acme-corp",
  "subscription_id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3",
  "status": "draft",
  "currency": "usd",
  "subtotal": { "amount": 4999, "currency": "usd", "display": "$49.99" },
  "tax_amount": { "amount": 0, "currency": "usd", "display": "$0.00" },
  "discount_amount": { "amount": 0, "currency": "usd", "display": "$0.00" },
  "total": { "amount": 4999, "currency": "usd", "display": "$49.99" },
  "line_items": [
    {
      "id": "li_01hqx3qgf2c8mhwy9s6zwmf3yi",
      "description": "Base subscription fee",
      "quantity": 1,
      "unit_amount": { "amount": 4999, "currency": "usd" },
      "amount": { "amount": 4999, "currency": "usd" },
      "type": "base"
    }
  ],
  "period_start": "2025-01-15T00:00:00Z",
  "period_end": "2025-02-15T00:00:00Z",
  "app_id": "myapp",
  "created_at": "2025-02-15T10:00:00Z",
  "updated_at": "2025-02-15T10:00:00Z"
}

Errors:

StatusCodeCondition
400invalid_inputMissing subscription_id
404subscription_not_foundSubscription does not exist
404plan_not_foundAssociated plan does not exist

GET /ledger/invoices

List invoices for the current tenant.

Query parameters:

ParameterTypeDescription
statusstringFilter by status: draft, pending, paid, past_due, voided
startdatetimeFilter by period start (ISO 8601)
enddatetimeFilter by period end (ISO 8601)
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of Invoice objects.


GET /ledger/invoices/{id}

Get an invoice by ID.

Response 200 OK -- Invoice object with full line items.

Errors: 404 Not Found if invoice does not exist.


GET /ledger/invoices/{number}

Get an invoice by invoice number. Invoice numbers are human-readable identifiers (e.g., INV-2025-0042).

Response 200 OK -- Invoice object.

Errors: 404 Not Found if invoice does not exist.


POST /ledger/invoices/{id}/finalize

Finalize a draft invoice. Finalized invoices cannot be modified and are ready for payment collection.

Response 200 OK -- Invoice object with status: "pending".

Errors:

StatusCodeCondition
400invoice_finalizedInvoice is already finalized
400invoice_incompleteInvoice has incomplete data
404invoice_not_foundInvoice does not exist

POST /ledger/invoices/{id}/void

Void an invoice. Voided invoices cannot be paid or modified.

Request body:

{
  "reason": "Customer requested cancellation"
}

Response 200 OK -- Invoice object with status: "voided".

Errors:

StatusCodeCondition
400invoice_paidCannot void a paid invoice
400invoice_voidedInvoice is already voided
404invoice_not_foundInvoice does not exist

POST /ledger/invoices/{id}/pay

Record a manual payment for an invoice.

Request body:

{
  "payment_ref": "ch_3P2x4N2eZvKYlo2C0r4JqHBX",
  "paid_at": "2025-02-15T12:00:00Z"
}

Response 200 OK -- Invoice object with status: "paid".

Errors:

StatusCodeCondition
400invoice_paidInvoice is already paid
400invoice_voidedCannot pay a voided invoice
404invoice_not_foundInvoice does not exist

GET /ledger/invoices/{id}/pdf

Download a PDF rendering of an invoice.

Response 200 OK with Content-Type: application/pdf.

Requires an InvoiceFormatter plugin registered with format "pdf".

Errors:

StatusCodeCondition
404invoice_not_foundInvoice does not exist
501not_implementedNo PDF formatter plugin registered

Payments

Process and manage payments through Stripe, Paddle, or other providers. See Payments for details.

POST /ledger/payments

Create a payment for an invoice. Routes to the configured payment provider.

Request body:

{
  "invoice_id": "inv_01hqx3qgf2c8mhwy9s6zwmf3yh",
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "amount": { "amount": 4999, "currency": "usd" },
  "payment_method_id": "pm_1234567890",
  "idempotency_key": "inv:inv_01hqx3qgf2c8mhwy9s6zwmf3yh"
}

Response 201 Created

{
  "id": "pay_01hqx3qgw4ekqhxyz6z6r099xa",
  "invoice_id": "inv_01hqx3qgf2c8mhwy9s6zwmf3yh",
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "amount": { "amount": 4999, "currency": "usd", "display": "$49.99" },
  "status": "pending",
  "processor_name": "stripe",
  "processor_id": "pi_3P2x4N2eZvKYlo2C0r4JqHBX",
  "created_at": "2025-02-15T12:00:00Z"
}

Errors:

StatusCodeCondition
400invalid_inputMissing required fields
400invoice_paidInvoice is already paid
402payment_failedPayment processing failed
404invoice_not_foundInvoice does not exist
500provider_not_configuredNo payment provider configured

GET /ledger/payments

List payments for the current tenant.

Query parameters:

ParameterTypeDescription
statusstringFilter by status: pending, succeeded, failed, refunded, canceled
invoice_idstringFilter by invoice ID
customer_idstringFilter by customer ID
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of Payment objects.


GET /ledger/payments/{id}

Get a payment by ID.

Response 200 OK -- Payment object.

Errors: 404 Not Found if payment does not exist.


POST /ledger/payments/{id}/refund

Issue a full or partial refund for a payment.

Request body:

{
  "amount": { "amount": 2500, "currency": "usd" },
  "reason": "Customer requested partial refund"
}

Omit amount for a full refund.

Response 200 OK

{
  "id": "pay_01hqx3qgw4ekqhxyz6z6r099xa",
  "status": "refunded",
  "refunded_amount": { "amount": 2500, "currency": "usd", "display": "$25.00" },
  "refund_reason": "Customer requested partial refund"
}

Errors:

StatusCodeCondition
400invalid_statePayment is not in succeeded state
400invalid_amountRefund amount exceeds payment amount
404payment_not_foundPayment does not exist

POST /ledger/payment-methods

Add a payment method for a customer. The token is obtained from the payment provider's client-side SDK (e.g., Stripe.js).

Request body:

{
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "token": "tok_visa_4242",
  "set_default": true
}

Response 201 Created

{
  "id": "pm_01hqx3qgw4ekqhxyz6z6r099xb",
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "type": "card",
  "last4": "4242",
  "brand": "visa",
  "expiry_month": 12,
  "expiry_year": 2027,
  "is_default": true
}

GET /ledger/payment-methods

List payment methods for the current customer.

Query parameters:

ParameterTypeDescription
customer_idstringFilter by customer ID (required)

Response 200 OK -- Array of PaymentMethod objects.


DELETE /ledger/payment-methods/{id}

Remove a payment method.

Response 204 No Content

Errors:

StatusCodeCondition
400invalid_stateCannot delete the only payment method if there are pending invoices
404not_foundPayment method does not exist

POST /webhooks/stripe

Handle Stripe webhook events. Verifies the webhook signature and updates payment/invoice status accordingly.

Headers:

HeaderDescription
Stripe-SignatureStripe webhook signature for verification

Request body: Raw Stripe event JSON payload.

Handled events:

Event typeAction
payment_intent.succeededMark payment as succeeded, invoice as paid
payment_intent.payment_failedMark payment as failed
charge.refundedRecord refund

Response 200 OK on success, 400 Bad Request on invalid payload, 401 Unauthorized on invalid signature.


POST /webhooks/paddle

Handle Paddle webhook events. Verifies the webhook signature and processes payment notifications.

Headers:

HeaderDescription
Paddle-SignaturePaddle webhook signature for verification

Request body: Raw Paddle event JSON payload.

Response 200 OK on success, 400 Bad Request on invalid payload.


Customers

Manage customer profiles, billing addresses, and payment methods. See Customer Management for details.

POST /ledger/customers

Create a new customer.

Request body:

{
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "currency": "usd",
  "billing_address": {
    "line1": "123 Main St",
    "line2": "Suite 100",
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94105",
    "country": "US"
  },
  "tax_ids": [
    { "type": "us_ein", "value": "12-3456789" }
  ],
  "metadata": { "source": "signup" }
}

Response 201 Created

{
  "id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "tenant_id": "acme-corp",
  "app_id": "myapp",
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "currency": "usd",
  "balance": { "amount": 0, "currency": "usd", "display": "$0.00" },
  "billing_address": {...},
  "tax_ids": [...],
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}

Errors:

StatusCodeCondition
400invalid_inputMissing required fields (name, email)
400invalid_emailInvalid email format
409duplicate_customer_emailCustomer with this email already exists

GET /ledger/customers

List customers for the current app.

Query parameters:

ParameterTypeDescription
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of Customer objects.


GET /ledger/customers/{id}

Get a customer by ID.

Response 200 OK -- Customer object.

Errors: 404 Not Found if customer does not exist.


GET /ledger/customers/email/{email}

Look up a customer by email address.

Response 200 OK -- Customer object.

Errors: 404 Not Found if no customer with that email exists.


PUT /ledger/customers/{id}

Update a customer. Only provided fields are updated.

Request body:

{
  "name": "Alice Smith",
  "billing_address": {
    "line1": "456 Oak Ave",
    "city": "New York",
    "state": "NY",
    "postal_code": "10001",
    "country": "US"
  }
}

Response 200 OK -- Updated Customer object.

Errors: 404 Not Found if customer does not exist.


DELETE /ledger/customers/{id}

Delete a customer. Customers with active subscriptions cannot be deleted.

Response 204 No Content

Errors:

StatusCodeCondition
400has_active_subscriptionsCustomer has active subscriptions
404customer_not_foundCustomer does not exist

POST /ledger/customers/{id}/credit

Add account credit to a customer. Credits are applied automatically to future invoices.

Request body:

{
  "amount": { "amount": 10000, "currency": "usd" },
  "reason": "Loyalty credit"
}

Response 200 OK

{
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "balance": { "amount": -10000, "currency": "usd", "display": "-$100.00" },
  "credit_applied": { "amount": 10000, "currency": "usd", "display": "$100.00" }
}

Negative balance indicates available credit.


GET /ledger/customers/{id}/subscriptions

List all subscriptions for a customer.

Response 200 OK -- Array of Subscription objects.


GET /ledger/customers/{id}/invoices

List all invoices for a customer.

Query parameters:

ParameterTypeDescription
statusstringFilter by invoice status
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of Invoice objects.


GET /ledger/customers/{id}/usage

Get current period usage for all metered features.

Response 200 OK

{
  "customer_id": "cst_01hqx3qgn9v3rywqk8zxr7t9kj",
  "period_start": "2025-01-15T00:00:00Z",
  "period_end": "2025-02-15T00:00:00Z",
  "usage": {
    "api_calls": { "used": 45230, "limit": 100000, "remaining": 54770 },
    "storage_gb": { "used": 12, "limit": 50, "remaining": 38 }
  }
}

POST /ledger/customers/{id}/portal

Create a secure billing portal session for a customer. The portal allows customers to view invoices, update payment methods, and manage subscriptions.

Request body:

{
  "return_url": "https://myapp.com/billing"
}

Response 200 OK

{
  "url": "https://billing.example.com/portal/session/abc123",
  "expires_at": "2025-02-15T11:30:00Z"
}

GET /ledger/customers/{id}/export

Export all customer data for GDPR compliance. Includes customer profile, subscriptions, invoices, payments, usage events, and entitlement checks.

Response 200 OK

{
  "customer": {...},
  "subscriptions": [...],
  "invoices": [...],
  "payments": [...],
  "usage_events": [...],
  "entitlement_checks": [...],
  "exported_at": "2025-02-15T10:00:00Z"
}

Coupons

Create and manage discount codes. See Coupons for details.

POST /ledger/coupons

Create a new coupon.

Request body:

{
  "code": "SUMMER25",
  "name": "Summer 2025 Promotion",
  "type": "percentage",
  "percentage": 25,
  "currency": "usd",
  "max_redemptions": 100,
  "valid_from": "2025-06-01T00:00:00Z",
  "valid_until": "2025-09-01T00:00:00Z",
  "metadata": { "campaign": "summer_2025" }
}

For fixed-amount coupons, use "type": "amount" with an amount field:

{
  "code": "FLAT10",
  "name": "10 Dollars Off",
  "type": "amount",
  "amount": { "amount": 1000, "currency": "usd" },
  "currency": "usd",
  "max_redemptions": 500
}

Response 201 Created

{
  "id": "cpn_01hqx3qgw4ekqhxyz6z6r099xa",
  "code": "SUMMER25",
  "name": "Summer 2025 Promotion",
  "type": "percentage",
  "percentage": 25,
  "currency": "usd",
  "max_redemptions": 100,
  "times_redeemed": 0,
  "valid_from": "2025-06-01T00:00:00Z",
  "valid_until": "2025-09-01T00:00:00Z",
  "app_id": "myapp",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}

Errors:

StatusCodeCondition
400invalid_inputMissing required fields (code, type)
409already_existsCoupon code already exists

GET /ledger/coupons

List coupons for the current app.

Query parameters:

ParameterTypeDescription
activeboolFilter to only currently valid coupons
limitintMax results (default: 20)
offsetintPagination offset

Response 200 OK -- Paginated array of Coupon objects.


GET /ledger/coupons/{code}

Get a coupon by its code.

Response 200 OK -- Coupon object.

Errors: 404 Not Found if coupon code does not exist.


GET /ledger/coupons/id/{id}

Get a coupon by its ID.

Response 200 OK -- Coupon object.

Errors: 404 Not Found if coupon does not exist.


PUT /ledger/coupons/{id}

Update a coupon. Only provided fields are updated.

Request body:

{
  "name": "Extended Summer Promotion",
  "valid_until": "2025-10-01T00:00:00Z",
  "max_redemptions": 200
}

Response 200 OK -- Updated Coupon object.

Errors: 404 Not Found if coupon does not exist.


DELETE /ledger/coupons/{id}

Delete a coupon.

Response 204 No Content

Errors: 404 Not Found if coupon does not exist.


POST /ledger/coupons/{code}/redeem

Redeem a coupon for a subscription. Validates the coupon is active, not expired, and has remaining redemptions.

Request body:

{
  "subscription_id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3"
}

Response 200 OK

{
  "coupon_id": "cpn_01hqx3qgw4ekqhxyz6z6r099xa",
  "code": "SUMMER25",
  "discount_type": "percentage",
  "discount_value": 25,
  "subscription_id": "sub_01hqx3qg8w5kjyhwkrp5j4wsy3",
  "applied_at": "2025-06-15T10:00:00Z"
}

Errors:

StatusCodeCondition
400coupon_expiredCoupon has passed its valid_until date
400coupon_not_startedCoupon valid_from date is in the future
400coupon_exhaustedAll redemptions have been used
400coupon_invalidCoupon failed custom validation
404coupon_not_foundCoupon code does not exist
404subscription_not_foundSubscription does not exist

On this page

AuthenticationPaginationError response formatPlansPOST /ledger/plansGET /ledger/plansGET /ledger/plans/{name}PUT /ledger/plans/{name}DELETE /ledger/plans/{id}SubscriptionsPOST /ledger/subscriptionsGET /ledger/subscriptionsGET /ledger/subscriptions/{id}PUT /ledger/subscriptions/{id}POST /ledger/subscriptions/{id}/cancelPOST /ledger/subscriptions/{id}/pausePOST /ledger/subscriptions/{id}/resumePOST /ledger/subscriptions/{id}/change-planMeteringPOST /ledger/meter/eventsPOST /ledger/meter/events/batchGET /ledger/meter/usage/{subscriptionId}/{meter}GET /ledger/meter/usage/{subscriptionId}/{meter}/historyGET /ledger/meter/eventsEntitlementsPOST /ledger/entitlements/checkPOST /ledger/entitlements/check-batchGET /ledger/entitlements/{customerId}POST /ledger/entitlements/{customerId}/invalidateInvoicesPOST /ledger/invoicesGET /ledger/invoicesGET /ledger/invoices/{id}GET /ledger/invoices/{number}POST /ledger/invoices/{id}/finalizePOST /ledger/invoices/{id}/voidPOST /ledger/invoices/{id}/payGET /ledger/invoices/{id}/pdfPaymentsPOST /ledger/paymentsGET /ledger/paymentsGET /ledger/payments/{id}POST /ledger/payments/{id}/refundPOST /ledger/payment-methodsGET /ledger/payment-methodsDELETE /ledger/payment-methods/{id}POST /webhooks/stripePOST /webhooks/paddleCustomersPOST /ledger/customersGET /ledger/customersGET /ledger/customers/{id}GET /ledger/customers/email/{email}PUT /ledger/customers/{id}DELETE /ledger/customers/{id}POST /ledger/customers/{id}/creditGET /ledger/customers/{id}/subscriptionsGET /ledger/customers/{id}/invoicesGET /ledger/customers/{id}/usagePOST /ledger/customers/{id}/portalGET /ledger/customers/{id}/exportCouponsPOST /ledger/couponsGET /ledger/couponsGET /ledger/coupons/{code}GET /ledger/coupons/id/{id}PUT /ledger/coupons/{id}DELETE /ledger/coupons/{id}POST /ledger/coupons/{code}/redeem