Authentication & Security (APIM-first with optional AFD)#
PortalPay enforces Azure API Management (APIM) subscription-based authentication for all developer-facing APIs exposed on the APIM custom domain. Azure Front Door (AFD) is optional as a fallback path; when enabled, the APIM policy also permits calls carrying the AFD-injected
markup
header. Health checks are always allowed without a subscription key.x-edge-secretCore objectives:
- Replace legacy trustless header model with APIM subscriptions
- Make APIM custom domain the primary public gateway for developers
- Optional AFD fallback accepted via markupheader injected by AFD
x-edge-secret - Preserve JWT cookie authentication for in-app admin/UI operations
- Defense-in-depth remains: AFD/WAF (optional) → APIM → Backend
Gateway and Base URL#
- Public API gateway: markup
https://api.pay.ledger1.ai/portalpay - Health check: markup(no subscription required)
GET /portalpay/healthz - All developer API routes: prefix with markup(APIM policy rewrites to backend
/portalpay/api/*markup)/api/*
Examples:
- Inventory list: markup
GET https://api.pay.ledger1.ai/portalpay/api/inventory - Create order: markup
POST https://api.pay.ledger1.ai/portalpay/api/orders
Request Requirements (Developer APIs)#
- Header: markup(required for all non-health routes)
Ocp-Apim-Subscription-Key - Do not send wallet identity headers (e.g., markup). APIM policy strips these; wallet resolution is performed using your subscription context and stamped via
x-walletmarkupfor the backend.x-subscription-id - If AFD is used as a fallback path, APIM also accepts requests carrying the AFD-injected internal header markup. Clients should not send this header; it is injected by AFD only.
x-edge-secret
cURL examples:
bash# Health (no key required) curl -i https://api.pay.ledger1.ai/portalpay/healthz # Inventory (subscription key required) curl -i -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \ https://api.pay.ledger1.ai/portalpay/api/inventory # Create order (subscription key required) curl -i -X POST -H "Content-Type: application/json" \ -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \ https://api.pay.ledger1.ai/portalpay/api/orders \ -d '{"items":[{"sku":"COFFEE-001","qty":2}],"jurisdictionCode":"US-CA"}'
PowerShell example:
powershell$headers = @{ "Ocp-Apim-Subscription-Key" = $env:APIM_SUBSCRIPTION_KEY } Invoke-RestMethod -Uri "https://api.pay.ledger1.ai/portalpay/api/orders" -Method Post -Headers $headers -ContentType "application/json" -Body (@{ items = @(@{ sku = "COFFEE-001"; qty = 2 }) jurisdictionCode = "US-CA" } | ConvertTo-Json)
Headers and Identity#
- Required (developer APIs): markup
Ocp-Apim-Subscription-Key - Gateway-stamped identity for backend:
- markup: stamped by APIM after subscription validation; used by backend to resolve wallet and correlate requests
x-subscription-id
- Header stripping:
- Client-supplied wallet headers (e.g., markup,
x-walletmarkup) are stripped by APIM policy to prevent spoofingwallet
- Client-supplied wallet headers (e.g.,
- Optional AFD-only header:
- markup: injected by AFD and validated by APIM inbound policy when AFD is used; clients MUST NOT send this themselves
x-edge-secret
- Optional observability headers:
- markup,
X-RateLimit-Limitmarkup,X-RateLimit-RemainingmarkupX-RateLimit-Reset - markup(if enabled)
X-Correlation-Id
Status Codes Matrix#
- 200/201 Success: Valid subscription key (or AFD secret), sufficient scopes
- 401 Unauthorized: Missing/invalid APIM subscription key
- 403 Forbidden:
- Valid key but insufficient product/scope or disallowed action
- Request did not meet policy conditions (e.g., attempted to rely on stripped wallet headers)
- 429 Too Many Requests: Rate limit exceeded
- 500 Server Error: Unhandled error
Example 429 payload:
json{ "error": "rate_limited", "resetAt": 1698765432000 }
Health Checks#
- Endpoint: markup
GET /portalpay/healthz - Behavior: Returns 200 with a small JSON body (e.g., markup)
{"status":"ok"} - Access: Available without subscription key to simplify probes and quick checks
Wallet Resolution and Trust Model#
- Wallet identity for developer requests is resolved at the gateway based on your APIM subscription. APIM stamps markupfor the backend.
x-subscription-id - APIM inbound policy:
- Validates subscription key
- Strips client wallet headers
- Optionally permits AFD-origin traffic via markup(if AFD is used)
x-edge-secret
- Backend uses the stamped subscription identity to resolve the merchant wallet and enforce scopes.
- UI/admin endpoints continue to use JWT via markupcookies.
cb_auth_token
Authorization Scopes#
Developer endpoints require scopes attached to your APIM product/subscription, such as:
- markup,
inventory:readmarkupinventory:write - markup
orders:create - markup,
receipts:readmarkupreceipts:write - markup
shop:read - markup
split:read
Backend enforcement uses the caller context:
- APIM path: subscription identity from markup
x-subscription-id - JWT path: user roles from markup
cb_auth_token
Admin and JWT#
Sensitive admin operations (e.g., shop config writes) use JWT auth:
- Cookie: markup
cb_auth_token - Obtain via in-app login (EIP-712 signature / Thirdweb)
- CSRF protection applies
Example (JWT-only admin write):
typescript// Within PortalPay admin UI const res = await fetch("/api/shop/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ processingFeePct: 1.5 }), credentials: "include" // send cb_auth_token cookie });
Defense-in-Depth#
- Azure API Management
- Products, subscriptions, policies, quotas, rate limits
- Diagnostics/logging, request tracing
- Azure Front Door + WAF (optional)
- OWASP protections, TLS, header normalization
- Injects markupwhen used; APIM policy can validate it
x-edge-secret
- Secrets & Identity
- Managed identity for APIM and backend
- Secrets stored in Azure Key Vault
- Data Partitioning & Audit
- Cosmos DB partitioned by resolved merchant wallet
- Immutable audit trails, monitoring/alerts
Rate Limiting and Errors#
Gateway and backend enforce quotas and rate limits per subscription/wallet. Expect:
- markup: Missing/invalid subscription key
401 Unauthorized - markup: Insufficient scope or request violated policy
403 Forbidden - markup: Rate limit exceeded
429 Too Many Requests
Headers may include:
- markup
X-RateLimit-Limit - markup
X-RateLimit-Remaining - markup
X-RateLimit-Reset
Incident Response#
If a subscription key is compromised:
- Revoke the key in APIM (or via the PortalPay admin subscription panel)
- Rotate to a new key
- Audit recent requests and receipts
- Tighten rate limits, IP allowlists, and WAF rules if applicable
If admin JWT is compromised:
- Invalidate sessions and rotate credentials
- Review audit logs and configuration changes
- Enforce additional CSRF and mTLS checks if necessary
Summary#
- Primary gateway: markup
https://api.pay.ledger1.ai/portalpay - Authentication (developer): markupon all non-health routes
Ocp-Apim-Subscription-Key - Healthz: permitted without subscription
- AFD: optional fallback path; accepted via APIM policy using markup(injected by AFD)
x-edge-secret - Backend trust anchored on subscription identity stamped by APIM (markup); client wallet headers are stripped
x-subscription-id
