An investment in knowledge pays the best interest.
Benjamin Franklin

Receipts

View and manage receipts, generate payment payloads, and track payment status.

Overview#

Receipt APIs allow you to:

  • List recent receipts

  • Create a payment receipt payload (for QR or portal checkout)

  • Retrieve a specific receipt

  • Track receipt/payment status (polling or webhooks)

  • Configure post-payment redirect URLs

  • Log refunds (admin/JWT)

  • Generate terminal receipts (POS-style)

  • Base URL:
    markup
    https://surge.basalthq.com
  • Authentication (Developer APIs): All developer-facing 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. Clients must not send wallet identity headers; APIM strips wallet headers and stamps the resolved identity.

Gateway posture:

  • APIM custom domain is the primary client endpoint.
  • Azure Front Door (AFD) may be configured as an optional/fallback edge; if enabled, APIM will accept an internal
    markup
    x-edge-secret
    per policy.
  • Rate limiting headers may be present when enabled:
    • markup
      X-RateLimit-Limit
      ,
      markup
      X-RateLimit-Remaining
      ,
      markup
      X-RateLimit-Reset
  • Correlation header on some writes:
    markup
    x-correlation-id
Admin/sensitive operations are performed via the BasaltSurge web app using JWT cookies (
markup
cb_auth_token
) with CSRF protections and role checks. See
markup
../auth.md
.

GET /api/receipts#

Required scopes:
markup
receipts:read
(included in BasaltSurge Standard product)

List recent receipts for the merchant associated with your subscription.

GET/api/receipts

List Receipts

List recent receipts for your merchant account

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.

Query Parameters
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy.
cURL
curl -X GET "https://surge.basalthq.com/api/receipts"
Response Status
Response Headers
Response Body

Request#

Headers:

markup
Ocp-Apim-Subscription-Key: {your-subscription-key}

Query Parameters:

ParameterTypeRequiredDescription
markup
limit
integerNoNumber of receipts to return (default: 100, min: 1, max: 200)

Example Requests:

curl -X GET "https://surge.basalthq.com/api/receipts?limit=50" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"

Response#

Success (200 OK):

json
{
  "receipts": [
    {
      "receiptId": "R-123456",
      "totalUsd": 13.09,
      "currency": "USD",
      "lineItems": [
        { "label": "Espresso", "priceUsd": 7.00, "qty": 2 },
        { "label": "Tax", "priceUsd": 1.09 },
        { "label": "Processing Fee", "priceUsd": 0.50 }
      ],
      "createdAt": 1698765432000,
      "brandName": "BasaltSurge",
      "status": "paid"
    }
  ]
}

Degraded Mode (Cosmos unavailable):

json
{
  "receipts": [ "..."],
  "degraded": true,
  "reason": "cosmos_unavailable"
}

Response Headers (when enabled at gateway):

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

POST /api/receipts#

Required scopes:
markup
receipts:write
(included in BasaltSurge Standard product)
Create a receipt payload for a QR-code payment portal. The returned
markup
paymentUrl
can be displayed in your app or encoded as a QR code.
POST/api/receipts

Create Receipt Payload

Create a receipt payload for QR/portal checkout

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/receipts" -H "Content-Type: application/json" -d '{
  "id": "rcpt_12345",
  "lineItems": [
    {
      "label": "Sample Item",
      "priceUsd": 25
    },
    {
      "label": "Tax",
      "priceUsd": 2
    }
  ],
  "totalUsd": 27
}'
Response Status
Response Headers
Response Body

Native EUR receipts#

json
{
  "id": "EU-ORDER-1001",
  "currency": "EUR",
  "lineItems": [
    { "label": "Items", "amount": 100.00, "qty": 2 },
    { "label": "Tax", "amount": 20.00 }
  ],
  "total": 120.00
}

Here the items line is €100 for both units, and the receipt total is €120. All native amounts must be finite numbers with at most two decimal places. One currency applies to the whole receipt. Unsupported currencies, mixed native/USD fields, and mismatched totals return HTTP 400. Missing exchange rates return HTTP 503 without creating a receipt.

The server saves the original native amounts and a
markup
pricing
valuation (
markup
version
,
markup
currency
,
markup
usdPerUnit
,
markup
quotedAt
,
markup
provider
). GET responses include native
markup
total
and
markup
lineItems[].amount
, alongside real USD
markup
totalUsd
and
markup
lineItems[].priceUsd
. Native amounts are derived from current USD accounting after edits; original creation amounts remain in
markup
pricing.originalTotal
and
markup
pricing.originalLineItems
.
The valuation is fixed for this receipt and reused for EU Stripe source conversion and reconciliation. Create a new receipt ID to obtain a new valuation; replacing an unpaid native receipt through POST returns HTTP 409. Existing USD fields retain USD meaning even if an older integration also sends
markup
currency: "EUR"
; currency alone never reinterprets
markup
priceUsd
as euros. Existing shipping configuration, USD edit/refund inputs, and USD reporting keep their current units.

Fee−/fee+ calculations and the inverted credit/debit split routing are unchanged. Receipt currency does not select the customer's region or alter EU verification. Stripe still receives the existing fee-adjusted source amount and settlement uses the USDC actually delivered; the stored FX valuation is not a guarantee of Stripe's final exchange quote.

Request#

Headers:

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

Body (JSON):

json
{
  "id": "rcpt_12345",
  "lineItems": [
    { "label": "Sample Item", "priceUsd": 25.0 },
    { "label": "Tax", "priceUsd": 2.0 }
  ],
  "totalUsd": 27.0,
  "redirect_url": "https://shop.example.com/thank-you",
  "webhook_url": "https://shop.example.com/api/basaltsurge-webhook"
}

Fields:

FieldTypeRequiredDescription
markup
id
stringYesUnique receipt ID you assign
markup
lineItems
arrayYesLegacy
markup
{ label, priceUsd, qty? }
or native
markup
{ label, amount, qty? }
items; do not mix formats
markup
currency
stringNoDenomination of native
markup
amount
/
markup
total
fields:
markup
USD
(default) or
markup
EUR
, case-insensitive
markup
lineItems[].amount
numberNative formatExtended line amount in receipt currency, in whole cents. Includes quantity;
markup
qty
is descriptive and is not multiplied again. Negative discount lines are allowed.
markup
total
numberNoNative order total; defaults to the sum of line amounts. If supplied, must equal that sum.
markup
totalUsd
numberLegacy formatOrder total in USD, paired with
markup
priceUsd
lines. Do not supply alongside native amounts.
markup
redirect_url
stringNoHTTPS URL passed through to the Stripe Crypto Onramp session. After the buyer completes the Stripe-hosted flow, Stripe redirects them to this URL. Only applies to Stripe; other onramp providers do not support external redirects. 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
webhook_url
stringNoHTTPS endpoint to receive push notifications when receipt status changes. Webhooks are signed with your API key (same
markup
Ocp-Apim-Subscription-Key
used for authentication). Also accepted as
markup
webhookUrl
. See Webhooks Guide.
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
)

Example Requests:

curl -X POST "https://surge.basalthq.com/api/receipts" \
  -H "Content-Type: application/json" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
  -d '{
    "id": "rcpt_12345",
    "lineItems": [
      { "label": "Sample Item", "priceUsd": 25.0 },
      { "label": "Tax", "priceUsd": 2.0 }
    ],
    "totalUsd": 27.0
  }'

Response#

Created (201):

json
{
  "id": "rcpt_12345",
  "paymentUrl": "https://surge.basalthq.com/portal/rcpt_12345?recipient=0x...&redirect_url=https%3A%2F%2Fshop.example.com%2Fthank-you",
  "status": "pending",
  "redirectUrl": "https://shop.example.com/thank-you",
  "webhookUrl": "https://shop.example.com/api/basaltsurge-webhook"
}

Other responses:

  • 400:
    markup
    invalid_input
  • 401:
    markup
    unauthorized
  • 403:
    markup
    forbidden
  • 429:
    markup
    rate_limited
  • 500:
    markup
    Server error

Response Headers (when enabled at gateway):

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

GET /api/receipts/{id}#

Required scopes:
markup
receipts:read
(included in BasaltSurge Standard product)

Retrieve a specific receipt by ID.

GET/api/receipts/rcpt_12345

Get Receipt by ID

Retrieve a specific receipt (replace rcpt_12345 with actual ID)

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.

Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy.
cURL
curl -X GET "https://surge.basalthq.com/api/receipts/rcpt_12345"
Response Status
Response Headers
Response Body

Request#

Headers:

markup
Ocp-Apim-Subscription-Key: {your-subscription-key}

Path Parameters:

ParameterTypeRequiredDescription
markup
id
stringYesReceipt ID (e.g.,
markup
rcpt_12345
)

Example:

bash
curl -X GET "https://surge.basalthq.com/api/receipts/rcpt_12345" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"

Response#

Success (200 OK):

json
{
  "receiptId": "rcpt_12345",
  "totalUsd": 27.0,
  "currency": "USD",
  "lineItems": [
    { "label": "Sample Item", "priceUsd": 25.0 },
    { "label": "Tax", "priceUsd": 2.0 }
  ],
  "createdAt": 1698765432000,
  "brandName": "BasaltSurge",
  "status": "paid",
  "jurisdictionCode": "US-CA",
  "taxRate": 0.095,
  "taxComponents": ["state", "county", "district"],
  "transactionHash": "0x...",
  "transactionTimestamp": 1698765500000,
  "billingAddress": {
    "firstName": "John",
    "lastName": "Smith",
    "phone": "+15555555555",
    "email": "[email protected]",
    "line1": "123 Main St",
    "line2": "Apt 4B",
    "city": "Seattle",
    "state": "WA",
    "zip": "98101",
    "country": "US"
  }
}

Other responses:

  • 401:
    markup
    unauthorized
  • 403:
    markup
    forbidden
  • 404:
    markup
    Not found
  • 429:
    markup
    rate_limited

Response Headers (when enabled at gateway):

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

GET /api/receipts/status#

Required scopes:
markup
receipts:read
(included in BasaltSurge Standard product)

Check payment status for a receipt.

GET/api/receipts/status

Check Receipt Status

Check payment status for a receipt

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.

Query Parameters
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy.
cURL
curl -X GET "https://surge.basalthq.com/api/receipts/status"
Response Status
Response Headers
Response Body

Request#

Headers:

markup
Ocp-Apim-Subscription-Key: {your-subscription-key}

Query Parameters:

ParameterTypeRequiredDescription
markup
receiptId
stringYesReceipt ID to check

Example:

bash
curl -X GET "https://surge.basalthq.com/api/receipts/status?receiptId=rcpt_12345" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"

Response#

Success (200 OK - Paid):

json
{
  "id": "rcpt_12345",
  "status": "completed",
  "transactionHash": "0xabc123...",
  "currency": "USDC",
  "amount": 27.0
}

Failed (200 OK - Failed Transaction):

When a transaction fails (e.g. card decline, KYC requirement, limit threshold), the endpoint returns structured diagnostic fields:

json
{
  "id": "rcpt_12345",
  "status": "failed",
  "failureCode": "PORTAL_PAY_INSUFFICIENT_FUNDS",
  "failureCategory": "card_decline",
  "failureReason": "The payment method was declined due to insufficient available funds.",
  "failureAction": "Ask the customer to retry with another card or use an alternate payment method.",
  "currency": "USDC",
  "amount": 27.0,
  "transactionHash": null
}

Other responses:

  • 401:
    markup
    unauthorized
  • 403:
    markup
    forbidden
  • 404:
    markup
    Not found
  • 429:
    markup
    rate_limited

Status values:

  • markup
    generated
    ,
    markup
    pending
    ,
    markup
    completed
    ,
    markup
    failed
    ,
    markup
    refunded
    ,
  • markup
    tx_mined
    ,
    markup
    recipient_validated
    ,
    markup
    tx_mismatch

Failure Categories:

  • markup
    card_decline
    : Card issuer declines, incorrect CVC, insufficient funds, expired cards
  • markup
    compliance
    : Identity verification requirements, unreadable documents, sanctions
  • markup
    limits
    : Purchase amount exceeding tier limits or system bounds
  • markup
    blockchain
    : Insufficient gas/tokens, user cancelled wallet prompt, slippage
  • markup
    session
    : User abandoned or closed checkout window
  • markup
    system
    : Payment network timeout or processing error

Response Headers (when enabled at gateway):

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

POST /api/receipts/status#

Update receipt status (tracking and sensitive events).

  • Tracking statuses (e.g.,
    markup
    link_opened
    ,
    markup
    buyer_logged_in
    ,
    markup
    checkout_initialized
    ) may be allowed without JWT.
  • Sensitive transitions (e.g.,
    markup
    checkout_success
    ,
    markup
    refund_*
    ) require JWT and CSRF via the portal UI.

Request#

Headers:

markup
Content-Type: application/json
Ocp-Apim-Subscription-Key: {your-subscription-key}  # when invoked via developer path

Body (JSON):

json
{
  "receiptId": "rcpt_12345",
  "wallet": "0xMerchantWallet",
  "status": "link_opened"
}

Response#

Success (200 OK):

json
{ "ok": true }

Other responses:

  • 400:
    markup
    missing_receipt_id
    |
    markup
    invalid_wallet
    |
    markup
    missing_status
  • 403:
    markup
    forbidden
  • 429:
    markup
    rate_limited
  • 500:
    markup
    failed

Response Headers:

  • markup
    x-correlation-id

POST /api/receipts/refund (Admin – JWT)#

Log a refund entry for a receipt and update status history. This operation is performed by admins via the BasaltSurge web app and is not callable via a developer APIM key.

Request#

Headers:

markup
Content-Type: application/json
Cookie: cb_auth_token=...

Body (JSON):

json
{
  "receiptId": "rcpt_12345",
  "wallet": "0xMerchantWallet",
  "buyer": "0xBuyerWallet",
  "usd": 13.09,
  "items": [
    { "label": "Espresso", "priceUsd": 7.00, "qty": 1 }
  ],
  "txHash": "0x..."
}

Response#

Success (200 OK):

json
{ "ok": true }

Other responses:

  • 400:
    markup
    missing_receipt_id
    |
    markup
    invalid_wallet
    |
    markup
    invalid_buyer
    |
    markup
    invalid_usd
  • 403:
    markup
    forbidden
  • 429:
    markup
    rate_limited
  • 500:
    markup
    failed

Response Headers:

  • markup
    x-correlation-id

POST /api/receipts/terminal#

Generate a terminal receipt (single amount + optional tax/fees). Useful for POS-style flows.

(Admin – JWT) This operation is performed by admins via the BasaltSurge web app and is not callable via a developer APIM key. Client-provided
markup
x-wallet
is ignored; the authenticated wallet is used.
For native pricing, send
markup
amount
with
markup
currency: "EUR"
(or
markup
USD
, the default for
markup
amount
).
markup
amount
is the pre-tax/pre-fee base amount. For example:
markup
{ "amount": 100, "currency": "EUR", "taxRate": 0.20 }
. The server normalizes it before the existing tax/fee calculations and returns native and USD values. The legacy
markup
amountUsd
parameter remains USD; do not send both amount fields.

Request#

Headers:

markup
Content-Type: application/json
Cookie: cb_auth_token=...

Body (JSON):

json
{
  "amountUsd": 25.0,
  "label": "Terminal Sale",
  "currency": "USD",
  "jurisdictionCode": "US-CA",
  "taxRate": 0.095,
  "taxComponents": ["state", "county"]
}

Response#

Success (200 OK):

json
{
  "ok": true,
  "receipt": {
    "receiptId": "R-987654",
    "totalUsd": 27.38,
    "currency": "USD",
    "lineItems": [
      { "label": "Terminal Sale", "priceUsd": 25.0 },
      { "label": "Tax", "priceUsd": 2.38 }
    ],
    "createdAt": 1698765432000,
    "status": "generated"
  }
}

Other responses:

  • 400:
    markup
    wallet_required
    |
    markup
    invalid_amount
  • 403:
    markup
    split_required
  • 429:
    markup
    rate_limited
  • 500:
    markup
    failed

Response Headers:

  • markup
    x-correlation-id

Error Responses#

401 Unauthorized

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

403 Forbidden

json
{ "error": "forbidden", "message": "Insufficient scope or origin enforcement failed" }

429 Too Many Requests

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

404 Not Found

json
{ "error": "not_found", "message": "Receipt not found" }

400 Bad Request

json
{ "error": "invalid_input", "message": "Invalid request" }

Code Examples#

JavaScript/TypeScript (developer)#

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

// List recent receipts
export async function getRecentReceipts(limit = 50) {
  const res = await fetch(`${API_BASE}/api/receipts?limit=${limit}`, {
    headers: { 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY }
  });
  return res.json();
}

// Create a payment receipt payload (QR/portal) with redirect and webhook
export async function createReceipt(payload: {
  id: string;
  lineItems: { label: string; priceUsd: number }[];
  totalUsd: number;
  redirect_url?: string;
  webhook_url?: string;
}) {
  const res = await fetch(`${API_BASE}/api/receipts`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY
    },
    body: JSON.stringify(payload)
  });
  return res.json();
}

// Get a specific receipt by ID
export async function getReceipt(id: string) {
  const res = await fetch(`${API_BASE}/api/receipts/${id}`, {
    headers: { 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY }
  });
  return res.ok ? res.json() : null;
}

// Check payment status
export async function getReceiptStatus(receiptId: string) {
  const res = await fetch(`${API_BASE}/api/receipts/status?receiptId=${receiptId}`, {
    headers: { 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY }
  });
  return res.json();
}

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

Python (developer)#

python
import os, requests
KEY = os.environ['APIM_SUBSCRIPTION_KEY']
API_BASE = 'https://surge.basalthq.com'
SITE_BASE = 'https://surge.basalthq.com'

def get_recent_receipts(limit=50):
  r = requests.get(f'{API_BASE}/api/receipts', headers={'Ocp-Apim-Subscription-Key': KEY}, params={'limit': limit})
  return r.json()

def create_receipt(payload):
  r = requests.post(f'{API_BASE}/api/receipts',
    headers={'Content-Type': 'application/json', 'Ocp-Apim-Subscription-Key': KEY},
    json=payload
  )
  return r.json()

def get_receipt(receipt_id):
  r = requests.get(f'{API_BASE}/api/receipts/{receipt_id}', headers={'Ocp-Apim-Subscription-Key': KEY})
  return r.json() if r.ok else None

def get_receipt_status(receipt_id):
  r = requests.get(f'{API_BASE}/api/receipts/status', headers={'Ocp-Apim-Subscription-Key': KEY}, params={'receiptId': receipt_id})
  return r.json()

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

Notes on Auth Models#

  • Developer integrations must use
    markup
    Ocp-Apim-Subscription-Key
    . Wallet identity is resolved at the gateway based on your subscription; the backend trusts the resolved identity.
  • Admin/UI operations in BasaltSurge use JWT cookies (
    markup
    cb_auth_token
    ) with CSRF and role checks for sensitive actions (refunds, terminal, certain status transitions). These routes are not available via APIM subscription keys.
  • Client requests do not include wallet identity headers; APIM strips wallet headers and stamps the resolved identity.
Receipts | BasaltSurge Docs | BasaltSurge