Orders and payments
Integrate payment collection with orders using your own payment provider or managed payment sessions
An order and a payment work together on every purchase. The order records what the customer buys. The payment satisfies the payment requirement of that order, and the order cannot be submitted before it does. You collect the payment through your own provider, or through a managed payment session.
Core concept
Every order has requirements that must be met before submission:
requiresPayment: Whether the order needs payment before it can be fulfilledrequiresPaymentProfile: Whether a stored payment method is needed for future billingrequiresSigning: Whether the order requires a digital signature
Each requirement is NOT_REQUIRED, OPTIONAL, or REQUIRED. Read them after you calculate
the price. They tell you the correct submission flow. An order with a total of zero can need
no payment at all. Read the requirements, and do not assume that a payment is due.
The requirements are per order. One order can need a payment where the order before it did not.
Quick path
Create order
Create an order and add line items for the products the customer wants to buy.
Read price
Read the taxes and totals off the order to determine the amount due.
Check requirements
Inspect the order requirements to determine if payment, a payment profile, or signing is needed.
Collect payment
Collect payment through your own provider or use a managed payment session.
Submit order
Submit the order with the external payment reference to begin fulfillment. Orders paying through a managed payment session or payment link are submitted automatically once the payment succeeds.
Choosing a payment approach
You have three ways to collect a payment. Use your own payment infrastructure, or use a managed payment session.
Your own provider (Recommended)
Collect the payment on your own payment stack, such as Stripe, Adyen, or Braintree. Pass the reference when you submit the order. You keep full control of the checkout, the payment methods, and the provider relationship.
Hosted payment page
Use a managed payment session with hosted: true. The response carries a checkout URL that the
provider hosts. Send the customer there. You build no payment interface.
Embedded payment widget
Use a managed payment session with hosted: false. The response carries the provider
credentials. Render the payment form in your own interface with the SDK of the provider.
When to use each approach
| Approach | Best for |
|---|---|
| Your own provider | A team that already runs a payment infrastructure and wants control of the provider and the checkout |
| Hosted payment page | A fast integration that needs no payment components of its own |
| Embedded widget | A team that wants a managed payment backend, but its own payment interface |
Most integrations collect the payment on their own provider. That way one payment relationship covers everything, and the checkout stays under their control.
1. Create an order and add line items
Create an order for a customer and include the products they want to purchase.
curl -X POST "{BASE_URL}/orders" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"customerId": "123e4567-e89b-12d3-a456-426614174000"
},
"lineItems": [
{
"type": "SUBSCRIPTION",
"lineItemId": "sub-1",
"productOfferingId": "offering-id",
"subscriber": {
"name": "Jane Doe",
"email": "jane@example.com"
}
}
]
}'const response = await fetch('{BASE_URL}/orders', {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer: {
customerId: '123e4567-e89b-12d3-a456-426614174000',
},
lineItems: [
{
type: 'SUBSCRIPTION',
lineItemId: 'sub-1',
productOfferingId: 'offering-id',
subscriber: {
name: 'Jane Doe',
email: 'jane@example.com',
},
},
],
}),
});
const order = await response.json();
const orderId = order.orderId;You can also add line items to an existing order separately:
curl -X POST "{BASE_URL}/orders/{orderId}/line-items" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "ADDON",
"lineItemId": "addon-1",
"productOfferingId": "addon-offering-id",
"parentLineItemId": "sub-1"
}'await fetch(`{BASE_URL}/orders/${orderId}/line-items`, {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'ADDON',
lineItemId: 'addon-1',
productOfferingId: 'addon-offering-id',
parentLineItemId: 'sub-1',
}),
});See Create Order and Add Line Items
2. Read the price
Taxes and totals are recalculated whenever the order changes, so read them off the order before collecting payment.
curl "{BASE_URL}/orders/{orderId}" \
-H "X-API-Key: $API_KEY"const orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
'X-API-Key': process.env.API_KEY,
},
});
const pricedOrder = await orderResponse.json();
// Amounts are integers in minor units: 2749 is $27.49
console.log('Total:', pricedOrder.pricing.totalMinor);
console.log('Currency:', pricedOrder.pricing.currency);See Get Order
3. Check order requirements
After calculating the price, inspect the order to determine what is needed before submission.
# Fetch the order and inspect the requirements object
curl "{BASE_URL}/orders/{orderId}" \
-H "X-API-Key: $API_KEY"const orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
'X-API-Key': process.env.API_KEY,
},
});
const orderDetails = await orderResponse.json();
const { requiresPayment, requiresPaymentProfile, requiresSigning } = orderDetails.requirements;
if (requiresPayment === 'REQUIRED') {
// Collect payment through your provider or via a payment session
}
if (requiresPaymentProfile === 'REQUIRED') {
// Set up a payment profile session for recurring billing
}
if (requiresSigning === 'REQUIRED') {
// Create a signing session for digital signature
}See Get Order
4. Collect payment
Once you know the order requires payment, choose one of the following approaches.
Option A: Your own payment provider (recommended)
Collect the payment through your own payment provider: Stripe, Adyen, Braintree, or another one. You keep full control of the checkout, and you keep the payment infrastructure that you already run. This approach adds no dependency.
After collecting payment on your side, submit the order with an externalPayment reference:
# Step 1: Collect payment through your own provider
# (This happens in your existing payment flow)
# Step 2: Submit the order with the payment reference
curl -X POST "{BASE_URL}/orders/{orderId}/submit" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalPayment": {
"reference": "pi_3ABC123def456",
"receiptDescription": "Subscription activation payment",
"receiptUrl": "https://yourapp.com/receipts/abc123"
}
}'// Step 1: Collect payment through your own provider
// (This happens in your existing payment flow)
// Step 2: Submit the order with the payment reference
const submitResponse = await fetch(`{BASE_URL}/orders/${orderId}/submit`, {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
externalPayment: {
reference: 'pi_3ABC123def456',
receiptDescription: 'Subscription activation payment',
receiptUrl: 'https://yourapp.com/receipts/abc123',
},
}),
});
const submittedOrder = await submitResponse.json();
console.log('Order state:', submittedOrder.state);The externalPayment object accepts:
| Field | Required | Description |
|---|---|---|
reference | Yes | The payment reference or transaction ID from your provider |
receiptDescription | No | A human-readable description of the payment |
receiptUrl | No | A URL to the payment receipt or confirmation page |
When using external payments, you are responsible for collecting the correct amount and handling refunds through your payment provider.
See Submit Order
Option B: Hosted payment page
If you prefer a managed payment flow, create a payment session with hosted: true to get a checkout URL. Redirect the customer to the provider-hosted payment page.
curl -X POST "{BASE_URL}/payment-sessions" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderId": "{orderId}",
"paymentProvider": "STRIPE",
"hosted": true,
"returnUrl": "https://yourapp.com/payment/success",
"cancelUrl": "https://yourapp.com/payment/cancel"
}'
# Redirect the customer to provider.checkoutUrl from the responseconst sessionResponse = await fetch('{BASE_URL}/payment-sessions', {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
orderId: orderId,
paymentProvider: 'STRIPE',
hosted: true,
returnUrl: 'https://yourapp.com/payment/success',
cancelUrl: 'https://yourapp.com/payment/cancel',
}),
});
const session = await sessionResponse.json();
// Redirect the customer to the hosted payment page
window.location.href = session.provider.checkoutUrl;After the customer completes payment, they are redirected to your returnUrl. There is no submit call to make — once the payment succeeds, the order is submitted automatically. Poll the order until it leaves PENDING_PAYMENT, or subscribe to the order.statusChanged webhook event.
# Poll the order state
curl "{BASE_URL}/orders/{orderId}" \
-H "X-API-Key: $API_KEY"
# When the payment succeeds, the state moves from PENDING_PAYMENT to SUBMITTEDconst orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
'X-API-Key': process.env.API_KEY,
},
});
const order = await orderResponse.json();
if (order.state === 'SUBMITTED') {
// Payment succeeded and the order is on its way to fulfillment
}See Create Payment Session and Get Payment Session
Option C: Embedded payment widget
Create a payment session with hosted: false (or omit the field) to get provider credentials for rendering a payment form directly in your UI.
curl -X POST "{BASE_URL}/payment-sessions" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orderId": "{orderId}",
"paymentProvider": "STRIPE"
}'
# Use provider.clientSecret and provider.publishableKey from the response
# to render a payment widgetconst sessionResponse = await fetch('{BASE_URL}/payment-sessions', {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
orderId: orderId,
paymentProvider: 'STRIPE',
}),
});
const session = await sessionResponse.json();
// Use the provider credentials to render a payment widget
const { clientSecret, publishableKey } = session.provider;Use the returned credentials with the provider’s client SDK. For example, with Stripe Elements:
const stripe = Stripe(publishableKey);
const elements = stripe.elements({ clientSecret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// When the customer submits the form:
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: 'https://yourapp.com/payment/success',
},
});After the payment completes, the order is submitted automatically — no submit call is needed. Poll the order until it leaves PENDING_PAYMENT, or subscribe to the order.statusChanged webhook event:
curl "{BASE_URL}/orders/{orderId}" \
-H "X-API-Key: $API_KEY"
# When the payment succeeds, the state moves from PENDING_PAYMENT to SUBMITTEDconst orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
'X-API-Key': process.env.API_KEY,
},
});
const order = await orderResponse.json();
if (order.state === 'SUBMITTED') {
// Payment succeeded and the order is on its way to fulfillment
}Zero-total orders with payment profile
An order with a total of zero can still need a stored payment method, as a trial subscription does. Submit that order with a payment profile session ID.
curl -X POST "{BASE_URL}/orders/{orderId}/submit" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"paymentProfileSessionId": "profile-session-id"
}'const submitResponse = await fetch(`{BASE_URL}/orders/${orderId}/submit`, {
method: 'POST',
headers: {
'X-API-Key': process.env.API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
paymentProfileSessionId: 'profile-session-id',
}),
});Order states
| State | Description |
|---|---|
PENDING | The order is a cart. You can still change it |
PENDING_PAYMENT | The order is locked and waits for the payment |
SUBMITTED | You submitted the order for processing |
PENDING_APPROVAL | The order waits for an approval |
PROCESSING | The order is in fulfillment |
COMPLETED | The platform fulfilled the order |
CANCELLED | The order was canceled before it completed |
EXPIRED | The order expired after a period of inactivity |
FAILED | The fulfillment of the order failed |
Payment session statuses
| Status | Description |
|---|---|
PENDING | Session created, awaiting customer payment |
REQUIRES_ACTION | The customer has one more step, such as 3D Secure |
COMPLETED | Payment successfully collected |
FAILED | Payment failed |
Requirements reference
When you retrieve an order after calculating the price, the requirements object tells you what is needed before submission.
| Requirement | Values | Description |
|---|---|---|
requiresPayment | NOT_REQUIRED, OPTIONAL, REQUIRED | Whether payment must be collected |
requiresPaymentProfile | NOT_REQUIRED, OPTIONAL, REQUIRED | Whether a stored payment method is needed |
requiresSigning | NOT_REQUIRED, OPTIONAL, REQUIRED | Whether a digital signature is needed |