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 markupper policy if enabled).
x-edge-secret
cb_auth_token../auth.mdPOST /portalpay/api/orders#
Required scopes: orders:create
Generate an order/receipt from inventory items.
POST/api/ordersCreate 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 Bodyapplication/jsonUsing server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.cURLcurl -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:
markupContent-Type: application/json Ocp-Apim-Subscription-Key: {your-subscription-key}
Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | array | Yes | Array of items to include in order |
markup | string | No | Inventory item ID |
markup | string | No | Inventory item SKU (if ID not provided) |
markup | number | Yes | Quantity (≥ 1) |
markup | string | No | Tax jurisdiction code (e.g., "US-CA") |
markup | number | No | Manual tax rate override (0-1) |
markup | string[] | No | Specific 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#
jurisdictionCodebashcurl -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:
bashcurl -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:
bashcurl -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#
taxable: truetypescript// 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:
markupSubtotal: $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):
markupSubtotal: $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:
markuphttps://pay.ledger1.ai/pay/{receiptId}
Example:
markuphttps://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#
markupgenerated → pending → paid → completed ↓ refunded
| Status | Description |
|---|---|
markup | Order created, awaiting payment |
markup | Payment initiated |
markup | Payment confirmed on blockchain |
markup | Payment successfully split |
markup | 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– SKU or ID not found in inventory
inventory_item_not_found - markup– Split contract not configured for this merchant
split_required - markup– Invalid request parameters
invalid_input
Advanced Examples#
Mixed Taxable/Non-Taxable Items (curl)#
bashcurl -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)#
bashcurl -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)#
bashcurl -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#
typescriptconst 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#
pythonimport 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.
typescriptconst jurisdiction = 'US-CA'; const order = await createOrder(cart, jurisdiction);
Error Handling#
split_requiredtypescripttry { 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. Wallet identity is resolved automatically at the gateway based on your subscription; the backend trusts the resolved identity.
Ocp-Apim-Subscription-Key - Admin/UI operations performed in the PortalPay app use JWT cookies (markup) with CSRF and role checks; no subscription key is required for in-app admin flows.
cb_auth_token - Client requests do not include wallet identity; APIM strips wallet headers and stamps the resolved identity.
