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
}