Place an Order
Create a draft order, add subscription line items, validate and price the order, then submit it for fulfillment using the Connect API
Orders are the Connect API’s shopping cart. You create a draft order, progressively configure it 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 walks through one complete integration: a US consumer, Jane Smith, orders a new mobile subscription with a newly assigned phone number, delivered as an eSIM to her iPhone. Along the way it covers every decision point you will hit: existing versus new customers, the pre-order validation tools, price calculation, submission requirements, and cancellation.
Prerequisites
Before you begin, ensure you have:
- 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, check network coverage, and confirm the device supports eSIM 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.
Calculate the price
Get 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 |
The order has been submitted for processing. |
PENDING_APPROVAL |
The order needs admin or manager approval via POST /orders/{orderId}/approve before processing continues. |
PROCESSING |
The order is being fulfilled. |
COMPLETED |
The order was successfully fulfilled. |
CANCELLED |
The order was canceled before completion. |
EXPIRED |
The order expired due to inactivity. |
FAILED |
Order fulfillment failed. |
Draft orders expire. Every order carries an expiresAt timestamp that is automatically refreshed
on each update, so an actively edited cart stays alive while abandoned ones move 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 "https://apiv2.example.com/api/v2/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(
'https://apiv2.example.com/api/v2/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": {
"netPrice": 30,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": {
"period": "MONTHLY",
"interval": 1
}
}
}
],
"pagination": {
"nextCursor": null
}
}
Step 2: Verify customer input with the order tools
The order tools let 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 "https://apiv2.example.com/api/v2/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('https://apiv2.example.com/api/v2/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 suggestedAddress can come back even for valid input when the network registry has a more
precise or standardized version of the address. When present, prefer it — using the network’s own
formatting avoids downstream provisioning issues.
Check network coverage
Confirm the customer will actually get service at their address, and show them the expected quality per technology.
curl -X POST "https://apiv2.example.com/api/v2/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('https://apiv2.example.com/api/v2/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 her phone’s IMEI to confirm the device supports it. Some networks also require the IMEI later to activate the eSIM, so this is worth collecting up front.
curl -X POST "https://apiv2.example.com/api/v2/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('https://apiv2.example.com/api/v2/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 — but if your customer wants to bring their existing number, check it is portable before you collect their porting details.
curl -X POST "https://apiv2.example.com/api/v2/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('https://apiv2.example.com/api/v2/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 either the internal UUID or your own external reference ID; both are resolved automatically. - New customer — pass the customer’s details (
nameandcustomerTypeare required) and the customer is created as part of order fulfillment. If you include areferenceIdand a customer already exists with it, that customer is reused instead of creating a duplicate — safe to call from flows where you are not sure whether the customer exists yet.
The user field identifies who will use the services and 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 "https://apiv2.example.com/api/v2/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('https://apiv2.example.com/api/v2/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 "https://apiv2.example.com/api/v2/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(`https://apiv2.example.com/api/v2/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"
}
If the customer were porting in a number instead, the line item would look like this (US porting details require firstName, lastName, and address; tempNumber: true assigns a temporary number the customer can 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 contains anything shippable — a physical SIM ("esim": false) or hardware — the
order also needs a shipping object with a recipient name and address. Jane’s eSIM ships
nothing, so this order skips it.
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 "https://apiv2.example.com/api/v2/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(`https://apiv2.example.com/api/v2/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: Calculate the price
For US orders, taxes are calculated at the jurisdiction level from the order’s addresses, so call POST /orders/{orderId}/calculate-price to get the exact amount due before collecting payment. The call takes no request body. Results are cached — repeated calls on an unchanged order return the same result.
curl -X POST "https://apiv2.example.com/api/v2/orders/296af5b6-f3b3-4128-b307-5ddc9190502f/calculate-price" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const calculatePrice = async (orderId) => {
const response = await fetch(
`https://apiv2.example.com/api/v2/orders/${orderId}/calculate-price`,
{
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Price calculation failed: ${error.message}`);
}
const { pricing } = await response.json();
console.log(`Total due now: ${pricing.total} ${pricing.currency}`);
return pricing;
};{
"pricing": {
"subtotal": 30,
"taxAmount": 5.87,
"fees": 0,
"total": 35.87,
"taxIncluded": false,
"currency": "USD",
"recurringCosts": {
"subtotal": 30,
"total": 30,
"taxIncluded": false,
"billingCycle": {
"period": "MONTHLY",
"interval": 1
}
},
"lineItems": [
{
"lineItemId": "line-item-1",
"description": "Seamless 10GB",
"subtotal": 30,
"taxAmount": 5.87,
"taxIncluded": false,
"total": 35.87,
"recurringAmount": 30
}
],
"calculatedAt": "2026-07-12T17:05:00Z"
}
}
Invalid orders cannot be priced. If this call returns an error, fetch the order and resolve the
validation issues first. Also note that recurringCosts in the US omits taxAmount — taxes on
recurring charges are calculated at invoicing time, not estimated here.
Step 7: Meet the submission requirements and submit
The order’s requirements object tells you what must happen before submission. Each requirement is NOT_REQUIRED, OPTIONAL, or REQUIRED, and what you get depends on platform configuration and the contents of the order — a prepaid order with only free items might need nothing, while postpaid orders typically require card capture or signing.
| Requirement | When REQUIRED |
Provide on submit |
|---|---|---|
requiresPayment |
The order total must be paid before fulfillment | paymentSessionId from a completed payment session |
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 |
Creating and completing payment sessions and payment profile sessions is covered in Payment Processing; signing sessions are documented in the API reference. Alternatively, if the customer paid outside the platform, pass an externalPayment object (with at least a reference) instead of a paymentSessionId — the order is then treated as paid. The two cannot be combined.
Jane’s order has requiresPayment: "REQUIRED", so after her payment session completes, submit with its ID:
curl -X POST "https://apiv2.example.com/api/v2/orders/296af5b6-f3b3-4128-b307-5ddc9190502f/submit" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"paymentSessionId": "a1b2c3d4-e5f6-7890-1234-56789abcdef0"
}'const submitOrder = async (orderId, paymentSessionId) => {
const response = await fetch(`https://apiv2.example.com/api/v2/orders/${orderId}/submit`, {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({ paymentSessionId }),
});
if (response.status === 412) {
const error = await response.json();
throw new Error(`Order not ready to submit: ${error.message}`);
}
if (!response.ok) {
const error = await response.json();
throw new Error(`Submission failed: ${error.message}`);
}
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"
}
In a fully managed flow, the order may be auto-submitted as soon as all requirements are fulfilled
(for example on successful payment or signing) — in that case, treat a 409 on your own submit
call as “already submitted” and re-fetch the order.
Step 8: Track the order to completion
After submission the order moves through SUBMITTED → PROCESSING → COMPLETED (with possible detours to PENDING_APPROVAL or FAILED). Poll GET /orders/{orderId}, or subscribe to the order.statusChanged and order.lineItemStatusChanged webhook events to avoid polling.
curl -X GET "https://apiv2.example.com/api/v2/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(`https://apiv2.example.com/api/v2/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"
}
Each line item carries its own fulfillment status (PENDING, RUNNING, COMPLETED, FAILED),
and an order can reach COMPLETED while an individual line item is still RUNNING or has
FAILED — one failed item does not block the rest of the order. Check line item statuses before
telling the customer everything is live.
Canceling a draft order
If the customer abandons the purchase, cancel the order to release any reserved resources. Only orders in PENDING state can be canceled; the optional body accepts metadata for your own bookkeeping. Abandoned orders that are never canceled expire on their own via expiresAt.
curl -X POST "https://apiv2.example.com/api/v2/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(`https://apiv2.example.com/api/v2/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."
}
Statuses you should handle in the order flow:
| 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. |
All mutating order endpoints accept an X-Idempotency-Key header. Send a unique key per logical
operation and retries become safe: a repeated request with the same key returns the original
result, while a modified request with the same key is rejected with 409. Keys expire after 24
hours.