Skip to content

Payments API

Endpoints for managing plans, subscriptions, invoices, checkout, and coupons.

SDK guide: Payments SDK · Detailed docs: Payments

Authentication

All endpoints require a tenant header identifying the tenant.

  • Authenticated — any logged-in user (workspace member or individual user).
  • Admin / Privileged — only users with isPrivileged: true (admins, service accounts).

Two-Phase Flow

Subscribing and paying are two separate operations:

  1. Subscribe — create a pending subscription record (POST /api/subscriptions).
  2. Checkout — initiate a provider payment session (POST /api/checkout).

For static plans the steps can be collapsed into a single POST /api/checkout call. For dynamic plans step 1 is mandatory and an admin must set the amount before step 2 can proceed.

User  →  POST /api/subscriptions                       creates pending subscription
Admin →  PUT  /api/subscriptions/:id/dynamic-amount    sets the charge amount
User  →  POST /api/checkout { subscriptionId }         initiates payment

Plans

GET /api/plans/public

Returns plans visible to the public (no authentication required).

Query parameters

NameTypeDescription
isActivebooleanFilter by active status

Response 200

json
[
  {
    "_id": "plan_id",
    "name": "Pro",
    "monthlyPrice": 29,
    "yearlyPrice": 290,
    "currency": "USD",
    "dynamic": false,
    "isActive": true
  }
]

SDK: sdk.payments.getPlans(query)


GET /api/plans/:planId

Returns a single plan. Auth: authenticated.

Errors

CodeHTTPDescription
PLAN_NOT_FOUND404No plan with the given ID

SDK: sdk.payments.getPlan(planId)


POST /api/plans admin

Create a plan. Auth: admin.

Request body — plan fields (see IPlan type in global-types).

Response 200 — created plan object.

SDK: adminSdk.managePayments.createPlan(data)


PUT /api/plans/:planId admin

Update a plan. Auth: admin.

Response 200 — updated plan object.

SDK: adminSdk.managePayments.updatePlan(planId, data)


DELETE /api/plans/:planId admin

Delete a plan. Auth: admin.

SDK: adminSdk.managePayments.deletePlan(planId)


Subscriptions

GET /api/subscriptions/me

Returns the current user's active or trialing subscription. Auth: authenticated.

Response 200 — subscription object or null.

SDK: sdk.payments.getMySubscription()


GET /api/subscriptions

List subscriptions. Auth: authenticated. Regular users see only their own entity's subscriptions. Admins can filter by any entity.

Query parameters

NameTypeDescription
billableEntityTypeuser | workspaceFilter by entity type (admin only)
billableEntityIdstringFilter by entity ID (admin only)
statuspending | active | trialing | canceled | past_dueFilter by status

SDK: adminSdk.managePayments.getSubscriptions(query)


GET /api/subscriptions/:id

Returns a single subscription. Auth: authenticated (non-admins can only access their own entity's subscriptions).

Errors

CodeHTTPDescription
SUBSCRIPTION_NOT_FOUND404No subscription with the given ID
403Access denied

SDK: adminSdk.managePayments.getSubscription(subscriptionId)


POST /api/subscriptions

Create a pending subscription. Auth: authenticated.

Regular users can only create subscriptions for their own entity. Fields dynamicAmount, providerKind, billableEntityType, and billableEntityId are ignored from the request body for non-admins — the server derives them from the authenticated user.

Request body (regular user)

FieldTypeRequiredDescription
planIdstringyesID of the plan to subscribe to
billingCyclemonthly | yearlyyesBilling frequency
couponCodestringnoCoupon to apply

Request body (admin)

FieldTypeRequiredDescription
planIdstringyes
billingCyclemonthly | yearlyyes
billableEntityTypeuser | workspaceyes
billableEntityIdstringyes
dynamicAmountnumbernoPre-set dynamic amount (dynamic plans only)
couponCodestringno

Response 200 — created subscription object with status: "pending".

SDK (user): sdk.payments.subscribeToPlan(planId, billingCycle, couponCode?)
SDK (admin): adminSdk.managePayments.createSubscription(data)


PUT /api/subscriptions/:id/dynamic-amount admin

Set or update the dynamic amount on a pending subscription. Must be called before checkout for dynamic plans. Can also update the amount on an active subscription to reprice the next billing cycle.

Auth: admin.

Request body

FieldTypeRequiredDescription
amountnumberyesPositive number representing the charge amount

Errors

CodeHTTPDescription
SUBSCRIPTION_NOT_FOUND404No subscription with the given ID
INVALID_AMOUNT400Amount must be a positive number

SDK: adminSdk.managePayments.setSubscriptionDynamicAmount(subscriptionId, amount)


PUT /api/subscriptions/:id/cancel

Cancel a subscription. Auth: authenticated (non-admins can only cancel their own entity's subscriptions).

Response 200 — updated subscription object with status: "canceled".

SDK (user): sdk.payments.cancelSubscription(subscriptionId)
SDK (admin): adminSdk.managePayments.cancelSubscription(subscriptionId)


Checkout

POST /api/checkout

Initiate a payment provider checkout session. Auth: authenticated.

Returns a checkoutUrl (redirect-based providers: Paddle, PayPal) or a clientToken (SDK-based providers: Sumit) along with the subscriptionId.

Either subscriptionId or planId is required.

FieldTypeRequiredDescription
subscriptionIdstringconditionalID of an existing pending subscription. Required for dynamic plans. All plan/entity data is read from the subscription.
planIdstringconditionalPlan ID for inline checkout of static plans. Ignored when subscriptionId is provided.
billingCyclemonthly | yearlyconditionalRequired when using the planId path.
billableEntityTypeuser | workspacenoAdmin-only override.
billableEntityIdstringnoAdmin-only override.
couponCodestringnoDiscount coupon code.
successUrlstringnoOverride the redirect URL on success. Falls back to tenant payment configuration.
cancelUrlstringnoOverride the redirect URL on cancellation.
amountnumbernoAdmin only. For dynamic plans: creates a pending subscription with this amount and immediately initiates checkout.

Response 200

json
{
  "subscriptionId": "sub_id",
  "checkoutUrl": "https://checkout.provider.com/session/xxx",
  "clientToken": null
}

Errors

CodeHTTPDescription
PLAN_NOT_FOUND404Plan does not exist
PLAN_NOT_ACTIVE400Plan is inactive
DYNAMIC_PLAN_REQUIRES_SUBSCRIPTION400Dynamic plan checkout requires a pre-created subscription with dynamicAmount set by an admin
DYNAMIC_AMOUNT_NOT_SET400The subscription's dynamicAmount is missing or ≤ 0
SUBSCRIPTION_NOT_PENDING400The referenced subscription is not in pending status
ACTIVE_SUBSCRIPTION_EXISTS409The entity already has an active or trialing subscription
PAYMENTS_NOT_CONFIGURED500Tenant has no payment provider configured
MISSING_EXTERNAL_PRICE_ID400Plan has no external price ID for the configured provider
UNSUPPORTED_PROVIDER400The configured payment provider is not supported
COUPON_NOT_FOUND400Coupon code does not exist
COUPON_EXPIRED400Coupon has passed its expiry date
COUPON_NOT_YET_VALID400Coupon is not yet valid
COUPON_MAX_REDEMPTIONS400Coupon has reached its redemption limit
COUPON_NOT_APPLICABLE400Coupon does not apply to this plan

SDK (user): sdk.payments.checkout(params)
SDK (admin): adminSdk.managePayments.checkout(params)


PUT /api/checkout/:subscriptionId/cancel

Cancel a subscription initiated through checkout. Attempts provider-side cancellation before marking it locally. Auth: authenticated (non-admins can only cancel their own entity's subscriptions).


Invoices

GET /api/invoices

List invoices. Auth: authenticated. Non-admins see only their own entity's invoices.

Query parameters (admin only)

NameTypeDescription
billableEntityTypeuser | workspace
billableEntityIdstring
statuspaid | open | void

SDK (user): sdk.payments.getInvoices(query)
SDK (admin): adminSdk.managePayments.getInvoices(query)


GET /api/invoices/:invoiceId

Returns a single invoice. Auth: authenticated.

SDK: sdk.payments.getInvoice(invoiceId)


Coupons

POST /api/coupons/validate

Validate a coupon code before checkout. Auth: authenticated.

Request body

FieldTypeRequiredDescription
codestringyesCoupon code
planIdstringnoValidates that the coupon applies to this plan

Errors

CodeHTTPDescription
COUPON_NOT_FOUND400
COUPON_EXPIRED400
COUPON_NOT_YET_VALID400
COUPON_MAX_REDEMPTIONS400
COUPON_NOT_APPLICABLE400Coupon does not apply to the given plan

SDK: sdk.payments.validateCoupon(code, planId?)


GET /api/coupons admin

List coupons. Auth: admin.

NameTypeDescription
isActivebooleanFilter by active status

SDK: adminSdk.managePayments.getCoupons(query)


POST /api/coupons admin

Create a coupon. Auth: admin.

SDK: adminSdk.managePayments.createCoupon(data)


PUT /api/coupons/:couponId admin

Update a coupon. Auth: admin.

SDK: adminSdk.managePayments.updateCoupon(couponId, data)


DELETE /api/coupons/:couponId admin

Delete a coupon. Auth: admin.

SDK: adminSdk.managePayments.deleteCoupon(couponId)


Webhooks

POST /api/payments/webhooks/:providerKind

Receives payment provider webhook events. Verifies the webhook signature and updates subscription/invoice state.

Auth: none (provider-signed payload).

:providerKindDescription
sumitSumit recurring payment events
paypalPayPal Billing subscription events
paddlePaddle subscription and transaction events

All events are tracked for idempotency — duplicates are safely ignored.

Build SaaS Products Without Limits.