An investment in knowledge pays the best interest.
Benjamin Franklin

Receipts

Receipt APIs#

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

  • Log refunds (admin/JWT)

  • Generate terminal receipts (POS-style)

  • Base URL:
    markup
    https://api.pay.ledger1.ai/portalpay
  • 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 PortalPay web app using JWT cookies (
markup
cb_auth_token
) with CSRF protections and role checks. See
markup
../auth.md
.

GET /portalpay/api/receipts#

Required scopes: receipts:read

List recent receipts for the merchant associated with your subscription.

GET/portalpay/api/receipts

List Receipts

List recent receipts for your merchant account

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.

Query Parameters
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.
cURL
curl -X GET "https://api.pay.ledger1.ai/portalpay/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://api.pay.ledger1.ai/portalpay/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": "PortalPay",
      "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 /portalpay/api/receipts#

Required scopes: receipts:write

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/portalpay/api/receipts

Create Receipt Payload

Create a receipt payload for QR/portal checkout

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/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

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
}

Fields:

  • markup
    id
    (string, required): Unique receipt ID you assign
  • markup
    lineItems
    (array, required): Array of
    markup
    { label, priceUsd }
    items
  • markup
    totalUsd
    (number, required): Order total in USD

Example Requests:

curl -X POST "https://api.pay.ledger1.ai/portalpay/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://pay.ledger1.ai/pay/rcpt_12345",
  "status": "pending"
}

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 /portalpay/api/receipts/{id}#

Required scopes: receipts:read

Retrieve a specific receipt by ID.

GET/portalpay/api/receipts/rcpt_12345

Get Receipt by ID

Retrieve a specific receipt (replace rcpt_12345 with actual ID)

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.

Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.
cURL
curl -X GET "https://api.pay.ledger1.ai/portalpay/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://api.pay.ledger1.ai/portalpay/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": "PortalPay",
  "status": "paid",
  "jurisdictionCode": "US-CA",
  "taxRate": 0.095,
  "taxComponents": ["state", "county", "district"],
  "transactionHash": "0x...",
  "transactionTimestamp": 1698765500000
}

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 /portalpay/api/receipts/status#

Required scopes: receipts:read

Check payment status for a receipt.

GET/portalpay/api/receipts/status

Check Receipt Status

Check payment status for a receipt

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.

Query Parameters
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.
cURL
curl -X GET "https://api.pay.ledger1.ai/portalpay/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://api.pay.ledger1.ai/portalpay/api/receipts/status?receiptId=rcpt_12345" \
  -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"

Response#

Success (200 OK):

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

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

Response Headers (when enabled at gateway):

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

POST /portalpay/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 /portalpay/api/receipts/refund (Admin – JWT)#

Log a refund entry for a receipt and update status history. This operation is performed by admins via the PortalPay 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 /portalpay/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 PortalPay web app and is not callable via a developer APIM key. Client-provided
markup
x-wallet
is ignored; the authenticated wallet is used.

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://api.pay.ledger1.ai/portalpay';
const SITE_BASE = 'https://pay.ledger1.ai'; // 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)
export async function createReceipt(payload: {
  id: string;
  lineItems: { label: string; priceUsd: number }[];
  totalUsd: number;
}) {
  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}/pay/${receiptId}`;
}

Python (developer)#

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

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}/pay/{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 PortalPay 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.