telnesstech

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: 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, read the network coverage, and make sure that the device supports an eSIM. Do all three 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. Read the price

    Read 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:

StateMeaning
PENDINGDraft (cart) state. The order can be modified, priced, and submitted.
PENDING_PAYMENTThe order is locked and awaiting payment completion.
SUBMITTEDYou submitted the order for processing.
PENDING_APPROVALThe order needs admin or manager approval through POST /orders/{orderId}/approve before it continues.
PROCESSINGThe order is in fulfillment.
COMPLETEDThe order was successfully fulfilled.
CANCELLEDThe order was canceled before completion.
EXPIREDThe order expired due to inactivity.
FAILEDOrder 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": "..." }. The customerId accepts the internal UUID and your own external reference ID. An external reference ID needs the rid_ prefix, as in rid_crm-customer-12345, so that the API can tell it from a UUID.
  • New customer — pass the details of the customer. name and customerType are required. The API creates the customer as part of order fulfillment. If you also pass a referenceId that 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 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 "{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.

RequirementWhen REQUIREDProvide on submit
requiresPaymentThe order total must be paid before fulfillmentan externalPayment reference, or pay through a payment session — no submit call needed
requiresPaymentProfileA stored payment method is needed for future billingpaymentProfileSessionId from a completed profile session
requiresSigningThe customer must digitally sign the ordersigningSessionId 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 SUBMITTEDPROCESSINGCOMPLETED. 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:

StatusWhen it happens
400Malformed request — inspect details for the offending property.
401Missing or expired access token.
403The API key or token does not grant access to this resource.
404Unknown orderId or lineItemId.
409The order is not in a state that allows the operation — for example, modifying or canceling an order after submission.
412Submission preconditions are not met — the order is invalid or a REQUIRED requirement is unfulfilled. Re-fetch the order and inspect validation and requirements.
429Rate limited — back off and retry.
500Unexpected 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.

Next steps