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 markupper policy.
x-edge-secret - Rate limiting headers may be present when enabled:
- markup,
X-RateLimit-Limitmarkup,X-RateLimit-RemainingmarkupX-RateLimit-Reset
- Correlation header on some writes: markup
x-correlation-id
cb_auth_token../auth.mdGET /portalpay/api/receipts#
Required scopes: receipts:read
List recent receipts for the merchant associated with your subscription.
GET/portalpay/api/receiptsList 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 ParametersUsing server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.cURLcurl -X GET "https://api.pay.ledger1.ai/portalpay/api/receipts"Response Status—Response Headers—Response Body—
Request#
Headers:
markupOcp-Apim-Subscription-Key: {your-subscription-key}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | integer | No | Number 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
paymentUrlPOST/portalpay/api/receiptsCreate 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 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/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:
markupContent-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(string, required): Unique receipt ID you assign
id - markup(array, required): Array of
lineItemsmarkupitems{ label, priceUsd } - markup(number, required): Order total in USD
totalUsd
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-Limitmarkup,X-RateLimit-RemainingmarkupX-RateLimit-Reset
GET /portalpay/api/receipts/{id}#
Required scopes: receipts:read
Retrieve a specific receipt by ID.
GET/portalpay/api/receipts/rcpt_12345Get 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.cURLcurl -X GET "https://api.pay.ledger1.ai/portalpay/api/receipts/rcpt_12345"Response Status—Response Headers—Response Body—
Request#
Headers:
markupOcp-Apim-Subscription-Key: {your-subscription-key}
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | string | Yes | Receipt ID (e.g., markup ) |
Example:
bashcurl -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-Limitmarkup,X-RateLimit-RemainingmarkupX-RateLimit-Reset
GET /portalpay/api/receipts/status#
Required scopes: receipts:read
Check payment status for a receipt.
GET/portalpay/api/receipts/statusCheck 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 ParametersUsing server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.cURLcurl -X GET "https://api.pay.ledger1.ai/portalpay/api/receipts/status"Response Status—Response Headers—Response Body—
Request#
Headers:
markupOcp-Apim-Subscription-Key: {your-subscription-key}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | string | Yes | Receipt ID to check |
Example:
bashcurl -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,
generatedmarkup,pendingmarkup,completedmarkup,failedmarkup,refunded - markup,
tx_minedmarkup,recipient_validatedmarkuptx_mismatch
Response Headers (when enabled at gateway):
- markup,
X-RateLimit-Limitmarkup,X-RateLimit-RemainingmarkupX-RateLimit-Reset
POST /portalpay/api/receipts/status#
Update receipt status (tracking and sensitive events).
- Tracking statuses (e.g., markup,
link_openedmarkup,buyer_logged_inmarkup) may be allowed without JWT.checkout_initialized - Sensitive transitions (e.g., markup,
checkout_successmarkup) require JWT and CSRF via the portal UI.refund_*
Request#
Headers:
markupContent-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_idmarkup|invalid_walletmarkupmissing_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:
markupContent-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_idmarkup|invalid_walletmarkup|invalid_buyermarkupinvalid_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.
x-walletRequest#
Headers:
markupContent-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_requiredmarkupinvalid_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)#
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 // 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)#
pythonimport 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. Wallet identity is resolved at the gateway based on your subscription; the backend trusts the resolved identity.
Ocp-Apim-Subscription-Key - Admin/UI operations in PortalPay use JWT cookies (markup) with CSRF and role checks for sensitive actions (refunds, terminal, certain status transitions). These routes are not available via APIM subscription keys.
cb_auth_token - Client requests do not include wallet identity headers; APIM strips wallet headers and stamps the resolved identity.
