telnesstech

Orders and payments

Integrate payment collection with orders using your own payment provider or managed payment sessions

An order and a payment work together on every purchase. The order records what the customer buys. The payment satisfies the payment requirement of that order, and the order cannot be submitted before it does. You collect the payment through your own provider, or through a managed payment session.

Core concept

Every order has requirements that must be met before submission:

  • requiresPayment: Whether the order needs payment before it can be fulfilled
  • requiresPaymentProfile: Whether a stored payment method is needed for future billing
  • requiresSigning: Whether the order requires a digital signature

Each requirement is NOT_REQUIRED, OPTIONAL, or REQUIRED. Read them after you calculate the price. They tell you the correct submission flow. An order with a total of zero can need no payment at all. Read the requirements, and do not assume that a payment is due.

The requirements are per order. One order can need a payment where the order before it did not.

Quick path

  1. Create order

    Create an order and add line items for the products the customer wants to buy.

  2. Read price

    Read the taxes and totals off the order to determine the amount due.

  3. Check requirements

    Inspect the order requirements to determine if payment, a payment profile, or signing is needed.

  4. Collect payment

    Collect payment through your own provider or use a managed payment session.

  5. Submit order

    Submit the order with the external payment reference to begin fulfillment. Orders paying through a managed payment session or payment link are submitted automatically once the payment succeeds.

Choosing a payment approach

You have three ways to collect a payment. Use your own payment infrastructure, or use a managed payment session.

Your own provider (Recommended)

Collect the payment on your own payment stack, such as Stripe, Adyen, or Braintree. Pass the reference when you submit the order. You keep full control of the checkout, the payment methods, and the provider relationship.

Hosted payment page

Use a managed payment session with hosted: true. The response carries a checkout URL that the provider hosts. Send the customer there. You build no payment interface.

Embedded payment widget

Use a managed payment session with hosted: false. The response carries the provider credentials. Render the payment form in your own interface with the SDK of the provider.

When to use each approach

ApproachBest for
Your own providerA team that already runs a payment infrastructure and wants control of the provider and the checkout
Hosted payment pageA fast integration that needs no payment components of its own
Embedded widgetA team that wants a managed payment backend, but its own payment interface

Most integrations collect the payment on their own provider. That way one payment relationship covers everything, and the checkout stays under their control.

1. Create an order and add line items

Create an order for a customer and include the products they want to purchase.

curl -X POST "{BASE_URL}/orders" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "customerId": "123e4567-e89b-12d3-a456-426614174000"
    },
    "lineItems": [
      {
        "type": "SUBSCRIPTION",
        "lineItemId": "sub-1",
        "productOfferingId": "offering-id",
        "subscriber": {
          "name": "Jane Doe",
          "email": "jane@example.com"
        }
      }
    ]
  }'
const response = await fetch('{BASE_URL}/orders', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    customer: {
      customerId: '123e4567-e89b-12d3-a456-426614174000',
    },
    lineItems: [
      {
        type: 'SUBSCRIPTION',
        lineItemId: 'sub-1',
        productOfferingId: 'offering-id',
        subscriber: {
          name: 'Jane Doe',
          email: 'jane@example.com',
        },
      },
    ],
  }),
});

const order = await response.json();
const orderId = order.orderId;

You can also add line items to an existing order separately:

curl -X POST "{BASE_URL}/orders/{orderId}/line-items" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "ADDON",
    "lineItemId": "addon-1",
    "productOfferingId": "addon-offering-id",
    "parentLineItemId": "sub-1"
  }'
await fetch(`{BASE_URL}/orders/${orderId}/line-items`, {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'ADDON',
    lineItemId: 'addon-1',
    productOfferingId: 'addon-offering-id',
    parentLineItemId: 'sub-1',
  }),
});

See Create Order and Add Line Items

2. Read the price

Taxes and totals are recalculated whenever the order changes, so read them off the order before collecting payment.

curl "{BASE_URL}/orders/{orderId}" \
  -H "X-API-Key: $API_KEY"
const orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
  headers: {
    'X-API-Key': process.env.API_KEY,
  },
});

const pricedOrder = await orderResponse.json();
// Amounts are integers in minor units: 2749 is $27.49
console.log('Total:', pricedOrder.pricing.totalMinor);
console.log('Currency:', pricedOrder.pricing.currency);

See Get Order

3. Check order requirements

After calculating the price, inspect the order to determine what is needed before submission.

# Fetch the order and inspect the requirements object
curl "{BASE_URL}/orders/{orderId}" \
  -H "X-API-Key: $API_KEY"
const orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
  headers: {
    'X-API-Key': process.env.API_KEY,
  },
});

const orderDetails = await orderResponse.json();

const { requiresPayment, requiresPaymentProfile, requiresSigning } = orderDetails.requirements;

if (requiresPayment === 'REQUIRED') {
  // Collect payment through your provider or via a payment session
}

if (requiresPaymentProfile === 'REQUIRED') {
  // Set up a payment profile session for recurring billing
}

if (requiresSigning === 'REQUIRED') {
  // Create a signing session for digital signature
}

See Get Order

4. Collect payment

Once you know the order requires payment, choose one of the following approaches.

Collect the payment through your own payment provider: Stripe, Adyen, Braintree, or another one. You keep full control of the checkout, and you keep the payment infrastructure that you already run. This approach adds no dependency.

After collecting payment on your side, submit the order with an externalPayment reference:

# Step 1: Collect payment through your own provider
# (This happens in your existing payment flow)

# Step 2: Submit the order with the payment reference
curl -X POST "{BASE_URL}/orders/{orderId}/submit" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalPayment": {
      "reference": "pi_3ABC123def456",
      "receiptDescription": "Subscription activation payment",
      "receiptUrl": "https://yourapp.com/receipts/abc123"
    }
  }'
// Step 1: Collect payment through your own provider
// (This happens in your existing payment flow)

// Step 2: Submit the order with the payment reference
const submitResponse = await fetch(`{BASE_URL}/orders/${orderId}/submit`, {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    externalPayment: {
      reference: 'pi_3ABC123def456',
      receiptDescription: 'Subscription activation payment',
      receiptUrl: 'https://yourapp.com/receipts/abc123',
    },
  }),
});

const submittedOrder = await submitResponse.json();
console.log('Order state:', submittedOrder.state);

The externalPayment object accepts:

FieldRequiredDescription
referenceYesThe payment reference or transaction ID from your provider
receiptDescriptionNoA human-readable description of the payment
receiptUrlNoA URL to the payment receipt or confirmation page

When using external payments, you are responsible for collecting the correct amount and handling refunds through your payment provider.

See Submit Order

Option B: Hosted payment page

If you prefer a managed payment flow, create a payment session with hosted: true to get a checkout URL. Redirect the customer to the provider-hosted payment page.

curl -X POST "{BASE_URL}/payment-sessions" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "{orderId}",
    "paymentProvider": "STRIPE",
    "hosted": true,
    "returnUrl": "https://yourapp.com/payment/success",
    "cancelUrl": "https://yourapp.com/payment/cancel"
  }'

# Redirect the customer to provider.checkoutUrl from the response
const sessionResponse = await fetch('{BASE_URL}/payment-sessions', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    orderId: orderId,
    paymentProvider: 'STRIPE',
    hosted: true,
    returnUrl: 'https://yourapp.com/payment/success',
    cancelUrl: 'https://yourapp.com/payment/cancel',
  }),
});

const session = await sessionResponse.json();

// Redirect the customer to the hosted payment page
window.location.href = session.provider.checkoutUrl;

After the customer completes payment, they are redirected to your returnUrl. There is no submit call to make — once the payment succeeds, the order is submitted automatically. Poll the order until it leaves PENDING_PAYMENT, or subscribe to the order.statusChanged webhook event.

# Poll the order state
curl "{BASE_URL}/orders/{orderId}" \
  -H "X-API-Key: $API_KEY"

# When the payment succeeds, the state moves from PENDING_PAYMENT to SUBMITTED
const orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
  headers: {
    'X-API-Key': process.env.API_KEY,
  },
});

const order = await orderResponse.json();

if (order.state === 'SUBMITTED') {
  // Payment succeeded and the order is on its way to fulfillment
}

See Create Payment Session and Get Payment Session

Option C: Embedded payment widget

Create a payment session with hosted: false (or omit the field) to get provider credentials for rendering a payment form directly in your UI.

curl -X POST "{BASE_URL}/payment-sessions" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "{orderId}",
    "paymentProvider": "STRIPE"
  }'

# Use provider.clientSecret and provider.publishableKey from the response
# to render a payment widget
const sessionResponse = await fetch('{BASE_URL}/payment-sessions', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    orderId: orderId,
    paymentProvider: 'STRIPE',
  }),
});

const session = await sessionResponse.json();

// Use the provider credentials to render a payment widget
const { clientSecret, publishableKey } = session.provider;

Use the returned credentials with the provider’s client SDK. For example, with Stripe Elements:

const stripe = Stripe(publishableKey);
const elements = stripe.elements({ clientSecret });

const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');

// When the customer submits the form:
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: 'https://yourapp.com/payment/success',
  },
});

After the payment completes, the order is submitted automatically — no submit call is needed. Poll the order until it leaves PENDING_PAYMENT, or subscribe to the order.statusChanged webhook event:

curl "{BASE_URL}/orders/{orderId}" \
  -H "X-API-Key: $API_KEY"

# When the payment succeeds, the state moves from PENDING_PAYMENT to SUBMITTED
const orderResponse = await fetch(`{BASE_URL}/orders/${orderId}`, {
  headers: {
    'X-API-Key': process.env.API_KEY,
  },
});

const order = await orderResponse.json();

if (order.state === 'SUBMITTED') {
  // Payment succeeded and the order is on its way to fulfillment
}

See Create Payment Session

Zero-total orders with payment profile

An order with a total of zero can still need a stored payment method, as a trial subscription does. Submit that order with a payment profile session ID.

curl -X POST "{BASE_URL}/orders/{orderId}/submit" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "paymentProfileSessionId": "profile-session-id"
  }'
const submitResponse = await fetch(`{BASE_URL}/orders/${orderId}/submit`, {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    paymentProfileSessionId: 'profile-session-id',
  }),
});

Order states

StateDescription
PENDINGThe order is a cart. You can still change it
PENDING_PAYMENTThe order is locked and waits for the payment
SUBMITTEDYou submitted the order for processing
PENDING_APPROVALThe order waits for an approval
PROCESSINGThe order is in fulfillment
COMPLETEDThe platform fulfilled the order
CANCELLEDThe order was canceled before it completed
EXPIREDThe order expired after a period of inactivity
FAILEDThe fulfillment of the order failed

Payment session statuses

StatusDescription
PENDINGSession created, awaiting customer payment
REQUIRES_ACTIONThe customer has one more step, such as 3D Secure
COMPLETEDPayment successfully collected
FAILEDPayment failed

Requirements reference

When you retrieve an order after calculating the price, the requirements object tells you what is needed before submission.

RequirementValuesDescription
requiresPaymentNOT_REQUIRED, OPTIONAL, REQUIREDWhether payment must be collected
requiresPaymentProfileNOT_REQUIRED, OPTIONAL, REQUIREDWhether a stored payment method is needed
requiresSigningNOT_REQUIRED, OPTIONAL, REQUIREDWhether a digital signature is needed

Next steps