An investment in knowledge pays the best interest.
Benjamin Franklin

Orders

Order APIs#

Generate receipts and orders with automatic tax calculation and processing fees.

Overview#

The Order API generates itemized receipts from your inventory with automatic tax calculation based on jurisdiction and processing fee computation. Orders are the core of the payment flow.

  • Base URL:
    markup
    https://api.pay.ledger1.ai/portalpay
  • Authentication (Developer APIs): All requests require an Azure APIM subscription key header:
    • markup
      Ocp-Apim-Subscription-Key: {your-subscription-key}
  • Wallet identity is resolved automatically at the gateway based on your subscription; client requests MUST NOT include wallet identity headers. APIM strips wallet headers and stamps the resolved identity.
  • Gateway posture: APIM custom domain is the primary endpoint. Azure Front Door (AFD) may be configured as an optional/fallback edge (APIM accepts
    markup
    x-edge-secret
    per policy if enabled).
Admin/UI-only operations in the PortalPay web app use JWT cookies (
markup
cb_auth_token
) and CSRF protections. See
markup
../auth.md
for details.

POST /portalpay/api/orders#

Required scopes: orders:create

Generate an order/receipt from inventory items.

POST/api/orders

Create Order/Receipt

Generate an order with automatic tax calculation

Default is the APIM custom domain. For AFD, enter only the AFD endpoint host (e.g., https://afd-endpoint-pay-...) without any path; the /portalpay prefix is added automatically for /api/* and /healthz.

The key is kept only in memory while this page is open. Do not paste secrets on shared machines.

For public reads (GET /api/inventory, GET /api/shop/config), include the merchant wallet (0x-prefixed 40-hex). Non-GET requests should use JWT and will ignore this header.

Request Body
application/json
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.
cURL
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" -H "Content-Type: application/json" -d '{
  "items": [
    {
      "sku": "COFFEE-001",
      "qty": 2
    },
    {
      "sku": "PASTRY-001",
      "qty": 1
    }
  ],
  "jurisdictionCode": "US-CA"
}'
Response Status
Response Headers
Response Body

Prerequisites#

⚠️ Split contract must be configured before creating orders. See Split Contract APIs.

Request#

Headers:

markup
Content-Type: application/json
Ocp-Apim-Subscription-Key: {your-subscription-key}

Body Parameters:

ParameterTypeRequiredDescription
markup
items
arrayYesArray of items to include in order
markup
items[].id
stringNoInventory item ID
markup
items[].sku
stringNoInventory item SKU (if ID not provided)
markup
items[].qty
numberYesQuantity (≥ 1)
markup
jurisdictionCode
stringNoTax jurisdiction code (e.g., "US-CA")
markup
taxRate
numberNoManual tax rate override (0-1)
markup
taxComponents
string[]NoSpecific tax components to apply

Example Requests:

curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [
      { "sku": "COFFEE-001", "qty": 2 },
      { "sku": "PASTRY-001", "qty": 1 }
    ],
    "jurisdictionCode": "US-CA"
  }'

Response#

Success (200 OK):

json
{
  "ok": true,
  "receipt": {
    "receiptId": "R-123456",
    "totalUsd": 13.09,
    "currency": "USD",
    "lineItems": [
      {
        "label": "Espresso",
        "priceUsd": 7.00,
        "qty": 2,
        "thumb": "data:image/jpeg;base64,...",
        "itemId": "inv_abc123",
        "sku": "COFFEE-001"
      },
      {
        "label": "Croissant",
        "priceUsd": 4.50,
        "qty": 1,
        "thumb": "data:image/jpeg;base64,...",
        "itemId": "inv_def456",
        "sku": "PASTRY-001"
      },
      {
        "label": "Tax",
        "priceUsd": 1.09
      },
      {
        "label": "Processing Fee",
        "priceUsd": 0.50
      }
    ],
    "createdAt": 1698765432000,
    "brandName": "My Coffee Shop",
    "status": "generated",
    "jurisdictionCode": "US-CA",
    "taxRate": 0.095,
    "taxComponents": ["state", "county", "district"]
  }
}

Degraded Mode:

json
{
  "ok": true,
  "degraded": true,
  "reason": "cosmos_unavailable",
  "receipt": { "...": "..." }
}

Response Headers (when enabled at gateway)#

  • markup
    X-RateLimit-Limit
  • markup
    X-RateLimit-Remaining
  • markup
    X-RateLimit-Reset

Tax Calculation#

Automatic Tax#

If
markup
jurisdictionCode
is provided, the system looks up the tax rate from your shop configuration:
bash
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [{"sku": "COFFEE-001", "qty": 1}],
    "jurisdictionCode": "US-CA"
  }'

Manual Tax Rate#

Override automatic tax with a specific rate:

bash
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [{"sku": "COFFEE-001", "qty": 1}],
    "taxRate": 0.08
  }'

Specific Tax Components#

Apply only specific tax components from a jurisdiction:

bash
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [{"sku": "COFFEE-001", "qty": 1}],
    "jurisdictionCode": "US-CA",
    "taxComponents": ["state", "county"]
  }'

Tax-Exempt Items#

Only items marked
markup
taxable: true
in inventory are taxed:
typescript
// Taxable product
{ "sku": "FOOD-001", "taxable": true }  // Will be taxed

// Tax-exempt product
{ "sku": "MEDICINE-001", "taxable": false }  // Not taxed

Processing Fees#

Processing fees are automatically calculated and added to orders.

Fee Structure:

  • Base Fee: 0.5% (platform)
  • Merchant Add-On: Configurable in shop settings

Examples:

markup
Subtotal: $100.00
Tax (9.5%): $9.50
Subtotal with tax: $109.50
Processing Fee (0.5%): $0.55
Total: $110.05

With custom 1.5% merchant fee (2% total):

markup
Subtotal: $100.00
Tax (9.5%): $9.50
Subtotal with tax: $109.50
Processing Fee (2%): $2.19
Total: $111.69

Configure merchant processing fee in Shop Configuration.


Payment URL#

After generating an order, customers pay via:

markup
https://pay.ledger1.ai/pay/{receiptId}

Example:

markup
https://pay.ledger1.ai/pay/R-123456

The payment page displays:

  • Itemized receipt
  • QR code for mobile wallets
  • Currency selection (ETH, USDC, USDT, etc.)
  • Payment instructions

Order Status Flow#

markup
generated → pending → paid → completed
                   ↓
                refunded
StatusDescription
markup
generated
Order created, awaiting payment
markup
pending
Payment initiated
markup
paid
Payment confirmed on blockchain
markup
completed
Payment successfully split
markup
refunded
Order refunded

Error Responses#

401 Unauthorized

json
{ "error": "unauthorized", "message": "Missing or invalid subscription key" }

403 Forbidden

json
{ "error": "forbidden", "message": "Insufficient scope or not allowed" }

429 Too Many Requests

json
{ "error": "rate_limited", "resetAt": 1698765432000 }

400 Bad Request

json
{ "error": "items_required", "message": "At least one item is required" }

Other common error codes:

  • markup
    inventory_item_not_found
    – SKU or ID not found in inventory
  • markup
    split_required
    – Split contract not configured for this merchant
  • markup
    invalid_input
    – Invalid request parameters

Advanced Examples#

Mixed Taxable/Non-Taxable Items (curl)#

bash
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [
      { "sku": "FOOD-001", "qty": 1 },
      { "sku": "MEDICINE-001", "qty": 1 }
    ],
    "jurisdictionCode": "US-CA"
  }'

Response calculates tax only on taxable items:

json
{
  "lineItems": [
    { "label": "Food", "priceUsd": 10.00, "qty": 1 },
    { "label": "Medicine", "priceUsd": 15.00, "qty": 1 },
    { "label": "Tax", "priceUsd": 0.95 }
  ]
}

Bulk Orders (curl)#

bash
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [
      {"sku": "ITEM-001", "qty": 10},
      {"sku": "ITEM-002", "qty": 5},
      {"sku": "ITEM-003", "qty": 20}
    ],
    "jurisdictionCode": "US-NY"
  }'

Using Item IDs (curl)#

bash
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/orders" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "items": [
      { "id": "inv_abc123", "qty": 1 }
    ],
    "jurisdictionCode": "US-CA"
  }'

Code Examples#

JavaScript/TypeScript#

typescript
const APIM_SUBSCRIPTION_KEY = process.env.APIM_SUBSCRIPTION_KEY!;
const API_BASE = 'https://api.pay.ledger1.ai/portalpay';
const SITE_BASE = 'https://pay.ledger1.ai'; // payment UI

// Create order from cart
async function createOrder(cartItems: any[], jurisdiction?: string) {
  const res = await fetch(`${API_BASE}/api/orders`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY
    },
    body: JSON.stringify({
      items: cartItems.map(item => ({ sku: item.sku, qty: item.quantity })),
      jurisdictionCode: jurisdiction
    })
  });
  const data = await res.json();
  if (!data.ok) throw new Error(data.error || 'failed');
  return data.receipt;
}

function getPaymentUrl(receiptId: string) {
  return `${SITE_BASE}/pay/${receiptId}`;
}

async function createOrderWithTax(items: any[], taxRate: number) {
  const res = await fetch(`${API_BASE}/api/orders`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY
    },
    body: JSON.stringify({ items, taxRate })
  });
  const data = await res.json();
  return data.receipt;
}

async function checkout(cart: any[]) {
  const receipt = await createOrder(cart, 'US-CA');
  return {
    receiptId: receipt.receiptId,
    total: receipt.totalUsd,
    paymentUrl: getPaymentUrl(receipt.receiptId)
  };
}

Python#

python
import os, requests
APIM_SUBSCRIPTION_KEY = os.environ['APIM_SUBSCRIPTION_KEY']
API_BASE = 'https://api.pay.ledger1.ai/portalpay'
SITE_BASE = 'https://pay.ledger1.ai'

def create_order(cart_items, jurisdiction=None):
    items = [{'sku': item['sku'], 'qty': item['quantity']} for item in cart_items]
    payload = {'items': items}
    if jurisdiction:
        payload['jurisdictionCode'] = jurisdiction

    r = requests.post(
        f'{API_BASE}/api/orders',
        headers={'Content-Type': 'application/json', 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY},
        json=payload
    )
    data = r.json()
    if not data.get('ok'):
        raise Exception(data.get('error') or 'failed')
    return data['receipt']

def get_payment_url(receipt_id):
    return f'{SITE_BASE}/pay/{receipt_id}'

Best Practices#

Inventory Validation#

typescript
// Good: Check inventory first
const inventory = await listProducts();
const validItems = cart.filter(item => inventory.some(inv => inv.sku === item.sku));
const order = await createOrder(validItems);

// Bad: Blindly create order
const order2 = await createOrder(cart);  // May fail if SKU missing

Tax Jurisdiction#

Use customer location to set jurisdiction.

typescript
const jurisdiction = 'US-CA';
const order = await createOrder(cart, jurisdiction);

Error Handling#

Handle
markup
split_required
gracefully:
typescript
try {
  const order = await createOrder(cart);
} catch (e: any) {
  if ((e.message || '').includes('split_required')) {
    // Guide merchant to configure split
    // redirectTo('/setup/split');
  }
}

Notes on Auth Models#

  • Developer integrations must use
    markup
    Ocp-Apim-Subscription-Key
    . Wallet identity is resolved automatically at the gateway based on your subscription; the backend trusts the resolved identity.
  • Admin/UI operations performed in the PortalPay app use JWT cookies (
    markup
    cb_auth_token
    ) with CSRF and role checks; no subscription key is required for in-app admin flows.
  • Client requests do not include wallet identity; APIM strips wallet headers and stamps the resolved identity.