telnesstech

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: Bearer access token and an X-API-Key header
  • Product offerings: At least one AVAILABLE product offering to sell — see Product Management
  • Payment integration: If your orders require payment, a way to run payment sessions — see Payment Processing

Overview

  1. Choose a product offering

    List product offerings and pick the plan the customer is buying.

  2. Verify customer input with the order tools

    Validate the address, check network coverage, and confirm the device supports eSIM before you build the order.

  3. Create a draft order

    Start an order for an existing customer or create the customer together with the order.

  4. Add a subscription line item

    Attach the product offering, subscriber details, and SIM configuration.

  5. Review the validation state

    Fetch the order and resolve any missing fields or validation errors.

  6. Calculate the price

    Get the exact total including jurisdiction-level US taxes before asking the customer to pay.

  7. Meet the requirements and submit

    Complete payment, payment profile, or signing requirements, then submit the order.

  8. 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": "..." }. The customerId accepts either the internal UUID or your own external reference ID; both are resolved automatically.
  • New customer — pass the customer’s details (name and customerType are required) and the customer is created as part of order fulfillment. If you include a referenceId and 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 msisdn empty to have a number assigned automatically (what Jane does).
  • Pick a number from the number pool and pass both the msisdn and the leaseToken you received when leasing it.
  • Port in an existing number by setting the msisdn, portingRequested: true, and porting.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 SUBMITTEDPROCESSINGCOMPLETED (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.

Next Steps