Skip to main content

Required fields for subscription creation

A subscription POST /subscriptions call has a small set of fields that must be present and in the exact shape the API expects. Getting any of these wrong typically fails silently — the API returns a 400, or accepts the request but creates a subscription that never activates. This page lists them.

For the full request example with backend code, see Create a subscription.

The four things to get right

1. isTrialOnly must be present and explicit

{ "isTrialOnly": false }

isTrialOnly is required even when you do not want a trial. Omitting it returns a 400 from the API. Pass false when the subscription should be billed immediately, true for trial-only plans.

2. paymentInstrumentId must be a flat top-level string

{
"customerId": "cus_xxx",
"paymentInstrumentId": "inst_xxx"
}

It is a top-level string field on the subscription request body. It is not nested inside paymentInstruction or any other object — that shape is used for the transactions endpoint, not for subscriptions.

3. items must use { plan: { id } }, not a bare plan ID

{
"items": [
{ "plan": { "id": "plan_xxx" } }
]
}

Each item is an object with a plan field that wraps an id. Passing the plan ID directly ("items": ["plan_xxx"]) is not the supported shape.

4. Read recentInvoicePaymentFormUrl from the response when status: "pending"

This is not a request field, but it is the field on the response you must not ignore. When the subscription is created against a new (inactive) payment instrument, the response is:

{
"data": {
"id": "ord_xxx",
"status": "pending",
"recentInvoicePaymentFormUrl": "https://sandbox-portal.secure-payments.app/...",
"recentInvoiceId": "in_xxx"
}
}

Your backend reads recentInvoicePaymentFormUrl and surfaces it to the frontend as the URL to redirect the customer to. Skipping this step is the single most common reason subscriptions appear "stuck" — see Subscription troubleshooting.

Optional: Idempotency-Key header

Idempotency-Key is not required for POST /subscriptions, but it is recommended if your backend may retry the request (timeout, dropped connection). Pass the same value on the retry and PaymentsAI returns the original subscription instead of creating a duplicate.

The value should be deterministic for a given operation — for example, a SHA-256 of customerId:planId. A fresh random UUID per attempt defeats the purpose. Maximum 100 characters.

Minimal correct request body

{
"customerId": "cus_xxx",
"paymentInstrumentId": "inst_xxx",
"items": [
{ "plan": { "id": "plan_xxx" } }
],
"isTrialOnly": false
}