Orders

What an order is, its lifecycle, statuses, providers, line items, and amounts.

Overview

An order is the customer's cart. It represents the grouping of products, taxes, discounts, and tips that a customer is purchasing from a merchant. The order tracks what was bought, the amounts owed, and whether the customer has paid.

Orders are the base layer of the Decal platform. Every payment — whether initiated through a payment link, the API, or a POS terminal — ultimately creates or updates an order. The order itself is not the payment: it describes what the customer is buying and how much they owe. The actual movement of money is tracked separately by internal transaction records.

{
  "id": "ord_live_a1b2c3d4",
  "provider": "native",
  "status": "open",
  "paymentStatus": "unpaid",
  "currency": "USD",
  "items": [
    {
      "name": "Premium Plan (Annual)",
      "quantity": 1,
      "unitPrice": 9900,
      "totalPrice": 9900
    }
  ],
  "amounts": {
    "subtotal": 9900,
    "tax": 0,
    "discount": 0,
    "tip": 0,
    "total": 9900,
    "paid": 0
  },
  "payments": [],
  "createdAt": "2026-04-12T14:30:00Z",
  "updatedAt": "2026-04-12T14:30:00Z"
}

Order lifecycle

Orders have two orthogonal status fields that track different things:

status tracks the order's fulfillment lifecycle:

StatusMeaning
openOrder exists and is active. May be unpaid, partially paid, or fully paid.
completedOrder is fulfilled and closed.
cancelledOrder was voided or abandoned. Cannot be reopened.

paymentStatus tracks how much the customer has paid relative to the total:

Payment StatusMeaning
unpaidNo payment received.
partially_paidSome payment received, but less than the total.
paidCustomer paid the full amount (or more).
overpaidCustomer paid more than the total (e.g., included a tip beyond the order total).

These two fields are independent. An order can be open and paid (payment received but not yet fulfilled), or completed and paid (the typical terminal state).

                    ┌─────────────┐
                    │    open     │
                    │   unpaid    │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
              ▼            ▼            ▼
        ┌───────────┐ ┌────────┐ ┌───────────┐
        │   open    │ │  open  │ │   open    │
        │ partially │ │  paid  │ │ cancelled │
        │   paid    │ │        │ │  unpaid   │
        └─────┬─────┘ └───┬────┘ └───────────┘
              │            │
              ▼            ▼
        ┌───────────┐ ┌───────────┐
        │   open    │ │ completed │
        │   paid    │ │   paid    │
        └─────┬─────┘ └───────────┘
              │
              ▼
        ┌───────────┐
        │ completed │
        │   paid    │
        └───────────┘

An order transitions to completed after payment is confirmed and any fulfillment actions (like notifying an external POS or loading stored value) finish successfully. An unpaid order can be cancelled if the customer abandons checkout or the merchant voids it.

What's in an order

An order is composed of line items and financial totals:

Line items

Each item in the order has a name, quantity, unit price, and computed total price. Items can optionally include a description, image URL, product reference, and arbitrary metadata.

{
  "items": [
    {
      "name": "Cappuccino",
      "quantity": 2,
      "unitPrice": 550,
      "totalPrice": 1100
    },
    {
      "name": "Blueberry Muffin",
      "quantity": 1,
      "unitPrice": 400,
      "totalPrice": 400
    }
  ]
}

Amounts

All monetary amounts are in the smallest currency unit (cents for USD). This avoids floating-point rounding issues that plague decimal-based calculations. Amounts are grouped under the amounts object on every order:

FieldMeaningExample
subtotalSum of line item totals1500 ($15.00)
taxTotal tax applied128 ($1.28)
discountTotal discount applied200 ($2.00)
tipOptional customer gratuity0
totalsubtotal + tax - discount + tip — what the customer owes1428 ($14.28)
paidWhat has actually been received0 (unpaid)

amounts.paid is the source of truth for paymentStatus. When a payment is confirmed, amounts.paid is updated to reflect the actual amount received, and paymentStatus is recalculated:

  • amounts.paid == 0 → unpaid
  • 0 < amounts.paid < amounts.total → partially_paid
  • amounts.paid >= amounts.total → paid (or overpaid if strictly greater)

Taxes and discounts

Orders can include itemized tax and discount lines, each with a name, type, value, and scope:

Taxes have three types:

  • additive — added on top of the subtotal (e.g., 8.5% sales tax)
  • inclusive — already included in item prices (e.g., VAT)
  • fixed — a flat amount

Discounts have two types:

  • fixed — a flat dollar amount off
  • percentage — a percentage of the subtotal

Both taxes and discounts can be scoped to the entire order or to individual line_items.

Payments

Fetching an order with GET /v0/orders/{orderId} returns payments, a list of the order's completed payments. Each payment describes what the customer paid in the currency they paid in, which can differ from the order's currency. The order's amounts are always in the order's currency.

payments is only included when you fetch a single order. The responses from creating or updating an order leave it out, so a missing payments means it was not loaded, not that the order is unpaid.

FieldMeaning
idThe payment's transaction ID (tx_…)
methodHow the customer paid, e.g. stablecoins or card
paid.amountWhat the customer paid, in the smallest unit of paid.currency
paid.currencyThe currency the customer paid in
paid.exchangeRateOrder currency per one unit of paid.currency, as quoted; absent when the two match
tokensFor a stablecoin payment, the stablecoins that funded it, as the customer's wallet shows
networkReferenceFor a stablecoin payment, the on-chain transfer as <network>:<signature>
paidAtWhen the payment completed

A $11.56 order paid by card, in the order's own currency, so there is no exchange rate:

{
  "currency": "USD",
  "amounts": {
    "paid": 1156,
    "subtotal": 1156,
    "total": 1156
  },
  "payments": [
    {
      "id": "tx_test_o2fmqljcc3acrogu35k8rl2w",
      "method": "card",
      "paid": {
        "amount": 1156,
        "currency": "USD"
      },
      "paidAt": "2026-10-28T18:52:54.098Z"
    }
  ]
}

A £0.37 order paid with $0.50 of PYUSD:

{
  "currency": "GBP",
  "amounts": {
    "paid": 37,
    "subtotal": 37,
    "total": 37
  },
  "payments": [
    {
      "id": "tx_live_…",
      "method": "stablecoins",
      "paid": {
        "amount": 50,
        "currency": "USD",
        "exchangeRate": "0.7505000000"
      },
      "tokens": [{ "symbol": "PYUSD", "amount": "0.5" }],
      "networkReference": "solana:5Kz3…",
      "paidAt": "2026-10-28T10:00:05.000Z"
    }
  ]
}

Exchange rates

Orders are priced in their own currency, but a customer can pay in a different one if your organization accepts other currencies. The rate quoted at that moment is saved as paid.exchangeRate.

The rate reads from the customer's side: how much of the order's currency one unit of paid.currency was worth. In the example above, "0.7505000000" on a GBP order paid in USD means $1.00 = £0.7505. Multiply what the customer paid by the rate to see it in the order's currency:

Customer paid (paid.amount, paid.currency)× paid.exchangeRateIn the order's currency
50 USD cents ($0.50)× 0.7505≈ 37.5 pence (£0.375)

The result is close to the order total but rarely exact, because paid.amount is rounded to the smallest unit of the customer's currency. The order's amounts remain the figures to reconcile against; the rate explains how the customer's amount relates to them, and is not meant to compute amounts from.

  • Absent means no conversion. When the customer paid in the order's currency, paid.exchangeRate is omitted.
  • Fixed when the customer paid. The rate is the quote the customer accepted and never changes afterwards.
  • Not your settlement rate. Turning the payment into your settlement stablecoin is a separate conversion that runs after the payment, at its own rate.
  • A decimal string, with up to ten decimal places. Parse it with a decimal library rather than a float if you need it exactly.

Order providers

The provider field indicates where the order originated:

ProviderDescription
nativeCreated through Decal's systems or API. Line items, taxes, and discounts are stored in the Decal platform.
squareSynced from a Square POS terminal. The full Square order is stored as a snapshot; line items are extracted from that snapshot rather than stored in relational tables.

The provider determines how payment completion is communicated back to the originating system. For native orders, payment is recorded entirely within Decal.

For external platform orders (e.g. Square), Decal also records the payment the external platform's APIs so the POS reflects the completed transaction.

Customer information

Orders capture customer information as a point-in-time snapshot at payment time:

{
  "customer": {
    "email": "alice@example.com",
    "phone": "+15551234567",
    "name": "Alice Chen"
  },
  "shippingAddress": {
    "firstName": "Alice",
    "lastName": "Chen",
    "address1": "123 Main St",
    "city": "Brooklyn",
    "state": "NY",
    "postalCode": "11215",
    "country": "US",
    "phone": "+15551234567"
  }
}

The customer snapshot is immutable after payment — even if the customer later updates their profile, the order reflects who they were when they paid. This ensures receipts and records remain accurate.

Shipping and billing addresses are optional by default, but can be marked as required to ensure customers must enter all required information for their order (e.g. shipping address when you are taking payments for physical products that must be shipped to the customer).

Test vs. live mode

Every order ID encodes its mode: ord_test_* or ord_live_*. Test and live orders are fully segregated:

  • Test orders use sandbox payment methods and never settle real money. Use test mode during development and integration testing.
  • Live orders process real payments and settle to the merchant's account.

A test order cannot become a live order or vice versa. The mode is set at creation and is part of the order's identity.

Creating orders

Orders are created through two paths:

When a customer opens a payment link, an order is created automatically as part of the checkout session. The order's line items, price, and currency come from the link's configuration. This is the standard merchant flow: create a link, share it, customers pay.

Via API

For programmatic use cases, create orders directly:

POST /v0/orders
{
  "mode": "live",
  "items": [
    {
      "name": "Custom Widget",
      "quantity": 3,
      "unitPrice": 2500
    }
  ],
  "customerEmail": "bob@example.com"
}

Use direct API creation when you need full control over the order composition — for example, when building a custom cart experience or creating orders from a backend system.