Place an order
Create a draft order, add subscription line items, validate and price the order, then submit it for fulfillment using the API
Orders are the API’s shopping cart. You create a draft order, configure it step by step with line items and customer details, price it, and submit it for fulfillment. Until submission, everything is editable — nothing is provisioned and nothing is charged.
This guide takes you through one complete integration. A US consumer, Jane Smith, orders a new mobile subscription with a new phone number. The subscription is delivered as an eSIM to her iPhone.
On the way, the guide covers every decision that you meet. An existing customer or a new one. The pre-order validation tools. The price calculation, the submission requirements, and cancellation.
Prerequisites
You need all of these before you start:
- API credentials: Every request carries both an
Authorization: Beareraccess token and anX-API-Keyheader - Product offerings: At least one
AVAILABLEproduct offering to sell — see Product Management - Payment integration: If your orders require payment, a way to run payment sessions — see Payment Processing
Overview
Choose a product offering
List product offerings and pick the plan the customer is buying.
Verify customer input with the order tools
Validate the address, read the network coverage, and make sure that the device supports an eSIM. Do all three before you build the order.
Create a draft order
Start an order for an existing customer or create the customer together with the order.
Add a subscription line item
Attach the product offering, subscriber details, and SIM configuration.
Review the validation state
Fetch the order and resolve any missing fields or validation errors.
Read the price
Read the exact total, including jurisdiction-level US taxes, before asking the customer to pay.
Meet the requirements and submit
Complete payment, payment profile, or signing requirements, then submit the order.
Track the order to completion
Watch the order state until the subscription is created and activated.
The order lifecycle
An order’s state tells you exactly what you can do with it:
| State | Meaning |
|---|---|
PENDING | Draft (cart) state. The order can be modified, priced, and submitted. |
PENDING_PAYMENT | The order is locked and awaiting payment completion. |
SUBMITTED | You submitted the order for processing. |
PENDING_APPROVAL | The order needs admin or manager approval through POST /orders/{orderId}/approve before it continues. |
PROCESSING | The order is in fulfillment. |
COMPLETED | The order was successfully fulfilled. |
CANCELLED | The order was canceled before completion. |
EXPIRED | The order expired due to inactivity. |
FAILED | Order fulfillment failed. |
A draft order expires. Every order carries an expiresAt timestamp, and each update moves it
forward. A cart that nobody touches goes to EXPIRED.
Step-by-step implementation
Example responses in this guide are trimmed to the fields relevant to each step. The API always returns the complete object.
Step 1: Choose a product offering
List the product offerings available to your customer type. The productOfferingId you pick here is what you attach to the order’s line item. Filter by types=SUBSCRIPTION to only see plans that create a mobile subscription.
curl -X GET "{BASE_URL}/product-offerings?customerType=CONSUMER&types=SUBSCRIPTION" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const listSubscriptionOfferings = async () => {
const response = await fetch(
'{BASE_URL}/product-offerings?customerType=CONSUMER&types=SUBSCRIPTION',
{
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
if (!response.ok) {
throw new Error(`Failed to list product offerings: ${response.status}`);
}
const { items } = await response.json();
return items;
};Jane picks the 10 GB plan:
{
"items": [
{
"productOfferingId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"status": "AVAILABLE",
"name": "Seamless 10GB",
"description": "10GB of high-speed data with unlimited calls and texts",
"customerType": "CONSUMER",
"product": {
"productId": "9b2f80c4-6a1d-4e3b-8c5f-7d9e0a1b2c3d",
"internalName": "seamless_cell_10gb_us",
"type": "SUBSCRIPTION",
"category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL",
"networkProviderId": "tmobile-us",
"features": {
"dataMb": 10240,
"includedCallSeconds": 3600,
"includedSms": 500
}
},
"price": {
"netPriceMinor": 2999,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": {
"period": "MONTHLY",
"interval": 1
},
"standardDiscount": { "amountMinor": 500 },
"bindingContract": {
"duration": {
"unit": "MONTHS",
"value": 12
},
"discount": {
"amountMinor": 200
}
},
"customUpfrontPayment": {
"billingCycles": 3,
"discount": {
"amountMinor": 300,
"duration": { "unit": "MONTHS", "value": 3 }
}
},
"currencyOptionsMinor": {
"USD": 2999,
"SEK": 29900
}
}
}
],
"pagination": {
"nextCursor": null
}
}Step 2: Verify customer input with the order tools
With the order tools you validate customer input at form time, before it becomes a validation error on the order. All four are stateless POST endpoints — call them as often as you like.
Validate the service address
In the US, the subscriber’s address doubles as the E911 emergency address, so it must be precise. Validate it as soon as the customer types it.
curl -X POST "{BASE_URL}/tools/validate-address" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
}'const validateAddress = async (address) => {
const response = await fetch('{BASE_URL}/tools/validate-address', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ address }),
});
const result = await response.json();
if (result.suggestedAddress) {
console.log('Network suggests a standardized address:', result.suggestedAddress);
}
return result;
};{
"valid": true
}A response can carry a suggestedAddress for a valid input, when the network registry holds a
more exact form of the address. Take that form. The formatting of the network prevents a
provisioning fault later.
Check network coverage
Make sure that the customer gets service at their address. Show them the quality to expect on each technology.
curl -X POST "{BASE_URL}/tools/check-network-coverage" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
}'const checkCoverage = async (address) => {
const response = await fetch('{BASE_URL}/tools/check-network-coverage', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ address }),
});
const coverage = await response.json();
if (coverage.coverageLevel === 'NO_COVERAGE') {
throw new Error('No network coverage at this address');
}
return coverage;
};The coverageLevel is one of EXCELLENT, GOOD, FAIR, POOR, or NO_COVERAGE:
{
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
},
"coverageLevel": "EXCELLENT",
"networkProviderId": "tmobile-us"
}Check device eSIM support
Jane wants an eSIM, so look up the IMEI of her phone. The response says whether the device supports an eSIM. Some networks also need the IMEI later, to activate the eSIM, so collect it now.
curl -X POST "{BASE_URL}/tools/get-device-info" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"imei": "356938035643809"
}'const getDeviceInfo = async (imei) => {
const response = await fetch('{BASE_URL}/tools/get-device-info', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ imei }),
});
const device = await response.json();
if (!device.esim) {
console.log('Device does not support eSIM - offer a physical SIM instead');
}
return device;
};{
"imei": "356938035643809",
"tac": "35693803",
"esim": true,
"manufacturer": "Apple",
"model": "A2653",
"marketingName": "iPhone 15 Pro"
}Check porting eligibility (port-ins only)
Jane takes a new number, so this step does not apply to her. If your customer wants to bring their own number, make sure that the number is portable before you collect the porting details.
curl -X POST "{BASE_URL}/tools/check-porting-eligibility" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"msisdn": "+14155550188"
}'const checkPortingEligibility = async (msisdn) => {
const response = await fetch('{BASE_URL}/tools/check-porting-eligibility', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ msisdn }),
});
const result = await response.json();
if (!result.eligible) {
console.log('Number cannot be ported:', result.ineligibilityReason);
}
return result;
};{
"msisdn": "+14155550188",
"eligible": true,
"networkProviderId": "att-us"
}Step 3: Create a draft order
Every order needs a customerType (CONSUMER or BUSINESS). Everything else can be added later, but the customer field is where you make your first real decision:
- Existing customer — pass
"customer": { "customerId": "..." }. ThecustomerIdaccepts the internal UUID and your own external reference ID. An external reference ID needs therid_prefix, as inrid_crm-customer-12345, so that the API can tell it from a UUID. - New customer — pass the details of the customer.
nameandcustomerTypeare required. The API creates the customer as part of order fulfillment. If you also pass areferenceIdthat a customer already carries, the API takes that customer and creates no duplicate. You can call this from a flow that does not know whether the customer exists.
The user field names the person who uses the services. It follows the same pattern. Pass
userId for a returning user, or name and email to create one.
Jane is new, so we create both the customer and the user with the order:
curl -X POST "{BASE_URL}/orders" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customerType": "CONSUMER",
"customer": {
"name": "Jane Smith",
"customerType": "CONSUMER",
"referenceId": "crm-cust-84321",
"contact": {
"email": "jane.smith@example.com",
"msisdn": "+14155550123"
},
"billing": {
"method": "EMAIL_INVOICE",
"email": "jane.smith@example.com",
"currency": "USD",
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
}
},
"user": {
"name": "Jane Smith",
"email": "jane.smith@example.com",
"msisdn": "+14155550123"
},
"billing": {
"name": "Jane Smith",
"email": "jane.smith@example.com",
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
}
}'const createOrder = async () => {
const address = {
street1: '826 Valencia St',
city: 'San Francisco',
state: 'CA',
zip: '94110',
country: 'US',
};
const response = await fetch('{BASE_URL}/orders', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerType: 'CONSUMER',
customer: {
name: 'Jane Smith',
customerType: 'CONSUMER',
referenceId: 'crm-cust-84321',
contact: {
email: 'jane.smith@example.com',
msisdn: '+14155550123',
},
billing: {
method: 'EMAIL_INVOICE',
email: 'jane.smith@example.com',
currency: 'USD',
address,
},
},
user: {
name: 'Jane Smith',
email: 'jane.smith@example.com',
msisdn: '+14155550123',
},
billing: {
name: 'Jane Smith',
email: 'jane.smith@example.com',
address,
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Order creation failed: ${error.message}`);
}
return response.json();
};The response is a draft order in PENDING state. Note newCustomer: true — the customer record itself is created during fulfillment, so it has no customerId yet:
{
"orderId": "296af5b6-f3b3-4128-b307-5ddc9190502f",
"state": "PENDING",
"customer": {
"customerType": "CONSUMER",
"name": "Jane Smith",
"newCustomer": true
},
"user": {
"userId": "c47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Jane Smith",
"newUser": true
},
"lineItems": [],
"validation": {
"isValid": false,
"missingFields": ["lineItems"]
},
"requirements": {
"requiresPayment": "REQUIRED",
"requiresPaymentProfile": "NOT_REQUIRED",
"requiresSigning": "NOT_REQUIRED"
},
"createdAt": "2026-07-12T17:00:00Z",
"updatedAt": "2026-07-12T17:00:00Z",
"expiresAt": "2026-07-19T17:00:00Z"
}For an existing customer, the request collapses to:
{
"customerType": "CONSUMER",
"customer": {
"customerId": "b47ac10b-58cc-4372-a567-0e02b2c3d479"
}
}You can also pass initial lineItems and a promoCode directly in the create request. This guide
adds line items separately to show the progressive flow, but a single create call with everything
inline is equally valid.
Step 4: Add a subscription line item
Add the plan to the order with POST /orders/{orderId}/line-items. A SUBSCRIPTION line item requires type, a lineItemId you choose (unique within the order), and the productOfferingId. The subscriber and sim objects are required eventually — provide them here or fill them in later with an update.
For the phone number, you have three options:
- Leave
msisdnempty to have a number assigned automatically (what Jane does). - Pick a number from the number pool and pass both the
msisdnand theleaseTokenyou received when leasing it. - Port in an existing number by setting the
msisdn,portingRequested: true, andporting.details.
curl -X POST "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f/line-items" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"lineItem": {
"type": "SUBSCRIPTION",
"lineItemId": "line-item-1",
"productOfferingId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"subscriber": {
"name": "Jane Smith",
"email": "jane.smith@example.com",
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
},
"sim": {
"esim": true,
"imei": "356938035643809"
}
}
}'const addSubscriptionLineItem = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}/line-items`, {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
lineItem: {
type: 'SUBSCRIPTION',
lineItemId: 'line-item-1',
productOfferingId: '3f2504e0-4f89-41d3-9a0c-0305e82c3301',
subscriber: {
name: 'Jane Smith',
email: 'jane.smith@example.com',
address: {
street1: '826 Valencia St',
city: 'San Francisco',
state: 'CA',
zip: '94110',
country: 'US',
},
},
sim: {
esim: true,
imei: '356938035643809',
},
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to add line item: ${error.message}`);
}
return response.json();
};The response echoes the line item with its server-resolved fulfillment status:
{
"type": "SUBSCRIPTION",
"lineItemId": "line-item-1",
"productOfferingId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"subscriber": {
"name": "Jane Smith",
"email": "jane.smith@example.com",
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
},
"sim": {
"esim": true,
"imei": "356938035643809"
},
"status": "PENDING"
}A line item for a port-in looks like this instead. US porting details require firstName,
lastName, and address. With tempNumber: true the customer gets a temporary number to use
until the port completes.
{
"type": "SUBSCRIPTION",
"lineItemId": "line-item-1",
"productOfferingId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"msisdn": "+14155550188",
"portingRequested": true,
"tempNumber": true,
"porting": {
"details": {
"firstName": "Jane",
"lastName": "Smith",
"accountNumber": "7724318842",
"passcode": "4821",
"address": {
"street1": "826 Valencia St",
"city": "San Francisco",
"state": "CA",
"zip": "94110",
"country": "US"
}
}
},
"subscriber": {
"name": "Jane Smith",
"email": "jane.smith@example.com"
},
"sim": {
"esim": true,
"imei": "356938035643809"
}
}To change a line item while the order is still PENDING, use PUT /orders/{orderId}/line-items/{lineItemId}. To remove one, use DELETE /orders/{orderId}/line-items/{lineItemId}.
If the order carries anything to ship, add a shipping object with a recipient name and
address. A physical SIM ("esim": false) and hardware both ship. Jane takes an eSIM, so this
order needs no shipping object.
Step 5: Review the order’s validation state
Line items are returned as part of the order, so GET /orders/{orderId} is your single read for everything: line items, validation, requirements, and pricing.
curl -X GET "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const getOrder = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch order: ${response.status}`);
}
const order = await response.json();
if (!order.validation.isValid) {
console.log('Order-level missing fields:', order.validation.missingFields);
for (const item of order.validation.lineItemValidation ?? []) {
if (!item.isValid) {
console.log(`Line item ${item.lineItemId} missing:`, item.missingFields);
}
}
}
return order;
};Jane’s order is now complete and ready to submit:
{
"orderId": "296af5b6-f3b3-4128-b307-5ddc9190502f",
"state": "PENDING",
"customer": {
"customerType": "CONSUMER",
"name": "Jane Smith",
"newCustomer": true
},
"lineItems": [
{
"type": "SUBSCRIPTION",
"lineItemId": "line-item-1",
"productOfferingId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"sim": {
"esim": true,
"imei": "356938035643809"
},
"status": "PENDING"
}
],
"validation": {
"isValid": true
},
"requirements": {
"requiresPayment": "REQUIRED",
"requiresPaymentProfile": "NOT_REQUIRED",
"requiresSigning": "NOT_REQUIRED"
},
"createdAt": "2026-07-12T17:00:00Z",
"updatedAt": "2026-07-12T17:04:00Z",
"expiresAt": "2026-07-19T17:04:00Z"
}When something is missing, validation tells you exactly what, at both the order level and per line item:
{
"isValid": false,
"missingFields": ["billing.address"],
"lineItemValidation": [
{
"lineItemId": "line-item-1",
"isValid": false,
"missingFields": ["subscriber.name", "sim.iccid"]
}
]
}Fix missing fields with PUT /orders/{orderId} (order details) and PUT /orders/{orderId}/line-items/{lineItemId} (line item details), then re-fetch.
Step 6: Read the price
The platform calculates the price again each time the order changes, and returns it as pricing
on the order. Read the order to get the exact amount due before you collect the payment. On a US
order the tax is calculated per jurisdiction, from the addresses on the order. The address fields
in Step 5 must be correct before this number means anything.
curl "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const readPrice = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Reading the order failed: ${error.message}`);
}
const { pricing } = await response.json();
console.log(`Total due now: ${pricing.totalMinor / 100} ${pricing.currency}`);
return pricing;
};{
"pricing": {
"subtotalMinor": 2999,
"taxAmountMinor": 450,
"totalMinor": 2749,
"taxIncluded": false,
"currency": "USD",
"recurringCosts": {
"subtotalMinor": 2299,
"totalMinor": 2299,
"taxIncluded": false,
"billingCycle": {
"period": "MONTHLY",
"interval": 1
}
},
"lineItems": [
{
"lineItemId": "line-item-1",
"description": "Seamless 10GB",
"subtotalMinor": 2999,
"discounts": [
{ "name": "Standard discount", "amountMinor": 500 },
{ "name": "12-month commitment", "amountMinor": 200 }
],
"totalDiscountsMinor": 700,
"taxAmountMinor": 450,
"taxIncluded": false,
"totalMinor": 2749,
"recurringAmountMinor": 2299
}
],
"calculatedAt": "2026-07-12T17:05:00Z"
}
}This is where the discounts of the offering turn into money. The catalog listed Seamless 10GB
at 2999 in Step 1, and it still does. Here that 2999 is the subtotalMinor. The
standardDiscount and the bindingContract.discount of the offering come off as
totalDiscountsMinor. The platform calculates the tax on what is left, and totalMinor is the
amount to charge.
Three more discounts apply at this same point. A promo code on the order. A price list assigned to the customer. A discount on the subscription.
The platform cannot price an invalid order. If this operation returns an error, get the order and
correct the validation problems first.
Amounts are integers in the minor units of currency, so 2749 is $27.49. The subtotalMinor
field gives the amount before discounts and tax. The platform reports the discounts of each line
item, so the total is 2999, less the 700 of totalDiscountsMinor, plus the 450 of tax.
In the US, recurringCosts does not include taxAmountMinor. The platform calculates the tax on
a recurring charge when it makes the invoice. It does not estimate that tax here.
Step 7: Meet the submission requirements and submit
The requirements object of the order tells you what must happen before you submit it. Each
requirement is NOT_REQUIRED, OPTIONAL, or REQUIRED. What you get depends on the platform
configuration and on the contents of the order. A prepaid order of free items alone can need
nothing. A postpaid order normally requires a card capture or a signature.
| Requirement | When REQUIRED | Provide on submit |
|---|---|---|
requiresPayment | The order total must be paid before fulfillment | an externalPayment reference, or pay through a payment session — no submit call needed |
requiresPaymentProfile | A stored payment method is needed for future billing | paymentProfileSessionId from a completed profile session |
requiresSigning | The customer must digitally sign the order | signingSessionId from a completed signing session |
Payment processing covers how to create and complete a payment session and a payment profile session. The API reference documents a signing session.
An order that pays through a payment session or a payment link moves to PENDING_PAYMENT. The
platform submits it as soon as the payment succeeds. If the customer paid outside the platform,
pass an externalPayment object on submit, with a reference in it. The order then counts as
paid.
Jane’s order has requiresPayment: "REQUIRED" and she pays through a hosted payment session, so there is no submit call to make. Once her payment succeeds, the platform submits the order — poll it until it leaves PENDING_PAYMENT:
curl "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const waitForSubmission = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
const order = await response.json();
console.log(`Order ${order.orderId} is now ${order.state}`);
return order;
};{
"orderId": "296af5b6-f3b3-4128-b307-5ddc9190502f",
"state": "SUBMITTED",
"paymentSessionId": "a1b2c3d4-e5f6-7890-1234-56789abcdef0",
"submittedAt": "2026-07-12T17:08:00Z"
}The submit endpoint accepts an order in the PENDING state only, and a payment can start on a
complete order only. A payment session and a payment link run the same validation as a submit,
so an order in PENDING_PAYMENT is known to be submittable already. Call submit yourself when
the requirements are met outside a payment session: an externalPayment reference, a payment
profile session, or a signing session.
Step 8: Track the order to completion
After the submit, the order moves through SUBMITTED → PROCESSING → COMPLETED. It can
also go to PENDING_APPROVAL or FAILED on the way. Poll GET /orders/{orderId} for the
state. To poll nothing, subscribe to the order.statusChanged and
order.lineItemStatusChanged webhook events.
curl -X GET "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const trackOrder = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}`, {
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
const order = await response.json();
switch (order.state) {
case 'SUBMITTED':
case 'PROCESSING':
console.log('Order is being fulfilled');
break;
case 'PENDING_APPROVAL':
console.log('Order is awaiting approval');
break;
case 'COMPLETED':
for (const subscription of order.createdEntities?.subscriptions ?? []) {
console.log(
`Line item ${subscription.createdByLineItem} created subscription ${subscription.subscriptionId}`,
);
}
break;
case 'FAILED':
console.error('Order fulfillment failed');
break;
}
return order;
};On completion, createdEntities maps each line item to what it produced — Jane’s subscription, with her newly assigned number:
{
"orderId": "296af5b6-f3b3-4128-b307-5ddc9190502f",
"state": "COMPLETED",
"customer": {
"customerId": "b47ac10b-58cc-4372-a567-0e02b2c3d479",
"customerType": "CONSUMER",
"name": "Jane Smith",
"newCustomer": true
},
"lineItems": [
{
"type": "SUBSCRIPTION",
"lineItemId": "line-item-1",
"productOfferingId": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"status": "COMPLETED"
}
],
"createdEntities": {
"subscriptions": [
{
"subscriptionId": "d5f7a2b1-3c4e-4f5a-8b9c-0d1e2f3a4b5c",
"status": "ACTIVATED",
"msisdn": "+14155550111",
"display": "(415) 555-0111",
"createdByLineItem": "line-item-1"
}
]
},
"submittedAt": "2026-07-12T17:08:00Z",
"completedAt": "2026-07-12T17:11:00Z"
}Read the status of every line item before you tell the customer that the service is live. Each
line item carries its own fulfillment status: PENDING, RUNNING, COMPLETED, or FAILED. An
order reaches COMPLETED even when one line item is still RUNNING or already FAILED, because
one failed item does not block the others.
Canceling a draft order
If the customer abandons the purchase, cancel the order. The cancel releases the resources that
the order reserved. Only an order in the PENDING state can be canceled. The optional body
accepts metadata for your own bookkeeping. An abandoned order that nobody cancels expires by
itself at its expiresAt.
curl -X POST "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f/cancel" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"reason": "customer abandoned checkout"
}
}'const cancelOrder = async (orderId) => {
const response = await fetch(`{BASE_URL}/orders/${orderId}/cancel`, {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
metadata: { reason: 'customer abandoned checkout' },
}),
});
if (response.status === 409) {
throw new Error('Order is no longer in PENDING state and cannot be canceled');
}
const order = await response.json();
console.log(`Order ${order.orderId} is now ${order.state}`);
return order;
};{
"orderId": "296af5b6-f3b3-4128-b307-5ddc9190502f",
"state": "CANCELLED"
}Error handling
All order endpoints return a consistent error body with a human-readable message, a machine-readable code, optional per-field details, and a hint for resolution:
{
"message": "Order cannot be submitted",
"code": "failed_precondition",
"details": [
{
"message": "A completed payment session is required to submit this order",
"code": "missing_payment_session",
"property": "paymentSessionId"
}
],
"hint": "Fetch the order to review its validation state and requirements, then retry."
}Your order flow must handle these statuses:
| Status | When it happens |
|---|---|
400 | Malformed request — inspect details for the offending property. |
401 | Missing or expired access token. |
403 | The API key or token does not grant access to this resource. |
404 | Unknown orderId or lineItemId. |
409 | The order is not in a state that allows the operation — for example, modifying or canceling an order after submission. |
412 | Submission preconditions are not met — the order is invalid or a REQUIRED requirement is unfulfilled. Re-fetch the order and inspect validation and requirements. |
429 | Rate limited — back off and retry. |
500 | Unexpected server error — safe to retry. |
Every order endpoint that changes something accepts an X-Idempotency-Key header. Send one
unique key per logical operation, and a retry becomes safe. The same key on the same request
returns the original result. The same key on a modified request gets a 409. A key expires
after 24 hours.