An investment in knowledge pays the best interest.
Benjamin Franklin

Orders

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://surge.basalthq.com
  • 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 BasaltSurge web app use JWT cookies (
markup
cb_auth_token
) and CSRF protections. See
markup
../auth.md
for details.

POST /api/orders#

Required scopes:
markup
orders:create
(included in BasaltSurge Standard product)

Generate an order/receipt from inventory items.

POST/api/orders

Create Order/Receipt

Generate an order with automatic tax calculation

Default is APP_URL. API paths (e.g., /api/shop/config) are served directly by each container.

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.
cURL
curl -X POST "https://surge.basalthq.com/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

EUR-priced inventory and orders#

Store an explicit native inventory price through
markup
POST /api/inventory
:
json
{
  "sku": "EU-COFFEE",
  "name": "Coffee",
  "price": 5.00,
  "currency": "EUR",
  "stockQty": 100,
  "taxable": true
}

Then create the order:

json
{
  "currency": "EUR",
  "items": [{ "sku": "EU-COFFEE", "qty": 2 }],
  "taxRate": 0.20
}
The item line is €10 before tax and the existing fee policy. Orders reads the merchant's saved inventory price;
markup
currency
does not enable a buyer-supplied price override. All explicit native inventory prices must match the order currency. A mismatch or unsupported currency returns HTTP 400; an unavailable EUR exchange rate returns HTTP 503.
The server values native inventory prices in USD before applying the existing discounts, tax, fee−/fee+, and payment calculations. It stores that exchange rate on the receipt and reuses it when creating the EU Stripe session. The receipt response includes
markup
currency
, native
markup
total
and
markup
lineItems[].amount
, USD
markup
totalUsd
and
markup
lineItems[].priceUsd
, and the saved
markup
pricing
valuation. The portal defaults to the native receipt currency. The saved valuation stays fixed for the receipt; a new order obtains a fresh rate.
Compatibility: existing inventory
markup
priceUsd
always means USD, including legacy items tagged
markup
currency: "EUR"
. To establish a true EUR catalog price, explicitly save
markup
price
and
markup
currency
; do not send
markup
priceUsd
in that same inventory request. Requesting an EUR receipt for legacy USD inventory converts its USD value for presentation and does not reinterpret the stored number as euros. Existing modifier adjustments, fixed discounts, shipping configuration, and fields ending in
markup
Usd
retain their USD units. Updating inventory later through
markup
priceUsd
returns that item to legacy USD pricing.
Credit/ACH continue using
markup
splitConfig
/
markup
splitAddress
; debit/prepaid continue using
markup
splitConfigCredit
/
markup
splitAddressCredit
, with existing fallbacks. Receipt currency does not change customer-region selection, EU verification, or payment eligibility. Settlement still uses Stripe's actual delivered USDC amount. See Receipt APIs for the direct native receipt format and valuation details.

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
currency
stringNoNative receipt denomination:
markup
USD
or
markup
EUR
, case-insensitive. Defaults to the native catalog currency when present; omitted on legacy orders preserves existing behavior.
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
markup
redirect_url
stringNoOptional redirect URL for custom Stripe Crypto Onramp redirect (also accepted as
markup
redirectUrl
)
markup
returnUrl
stringNoOptional redirect URL. Customer's browser will be redirected here after successful payment (also accepted as
markup
return_url
)
markup
onSuccess
stringNoOptional custom logic. Can be a redirect URL or a raw JavaScript code snippet string to be evaluated after successful payment
markup
stripeEmail
stringNoBuyer's email to pre-populate in Stripe Link / credit card email field. Passing this allows the checkout portal to bypass the email prompt and automatically proceed.
markup
billingFirstName
stringNoBuyer's first name to pre-populate in the KYC/billing section (also accepted as
markup
customerFirstName
)
markup
billingLastName
stringNoBuyer's last name to pre-populate in the KYC/billing section (also accepted as
markup
customerLastName
)
markup
billingEmail
stringNoBuyer's email for billing/KYC section (also accepted as
markup
customerEmail
or
markup
stripeEmail
)
markup
billingPhone
stringNoBuyer's phone number to pre-populate in the KYC/billing section (also accepted as
markup
customerPhone
)
markup
billingAddressLine1
stringNoAddress line 1 for the billing address (also accepted as
markup
customerAddressLine1
)
markup
billingAddressLine2
stringNoAddress line 2 (Apt, Suite, Unit) for the billing address (also accepted as
markup
customerAddressLine2
)
markup
billingAddressCity
stringNoCity for the billing address (also accepted as
markup
customerAddressCity
)
markup
billingAddressState
stringNoState or region (2-letter code preferred) for the billing address (also accepted as
markup
customerAddressState
)
markup
billingAddressPostalCode
stringNoZip or postal code for the billing address (also accepted as
markup
billingAddressZip
or
markup
customerAddressPostalCode
)
markup
billingAddressCountry
stringNoCountry code (2-letter ISO, e.g. "US") for the billing address (also accepted as
markup
customerAddressCountry
)
markup
transactionId
stringNoOptional custom transaction or order reference ID from your system (also accepted as
markup
clientTransactionId
or
markup
externalTransactionId
)
markup
metadata
objectNoOptional key-value JSON object containing custom metadata (e.g.
markup
{ "orderRef": "ERP-101", "source": "mobile_app" }
)

Example Requests:

curl -X POST "https://surge.basalthq.com/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"],
    "transactionId": "TX-99882211",
    "metadata": {
      "orderRef": "ERP-PO-774",
      "customerTier": "vip"
    }
  }
}

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://surge.basalthq.com/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://surge.basalthq.com/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://surge.basalthq.com/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://surge.basalthq.com/portal/{receiptId}

Example:

markup
https://surge.basalthq.com/portal/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://surge.basalthq.com/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://surge.basalthq.com/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://surge.basalthq.com/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://surge.basalthq.com';
const SITE_BASE = 'https://surge.basalthq.com'; // 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}/portal/${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://surge.basalthq.com'
SITE_BASE = 'https://surge.basalthq.com'

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}/portal/{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 BasaltSurge 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.
Orders | BasaltSurge Docs | BasaltSurge