Inventory APIs#
Manage your product catalog with full CRUD operations.
Overview#
The Inventory APIs allow you to create, read, update, and delete products in your catalog. Products created here can be used when generating orders.
- Base URL: markup
https://api.pay.ledger1.ai/portalpay - 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; clients MUST NOT send wallet identity headers. APIM strips wallet headers and stamps the resolved subscription identity.
- Admin/UI writes through the PortalPay web app use JWT cookies (markup) and CSRF protections. See
cb_auth_tokenmarkupfor details.../auth.md
GET /portalpay/api/inventory#
Required scopes: inventory:read
List all products in your inventory with advanced filtering, sorting, and pagination.
GET/api/inventoryList Inventory Products
Retrieve products with optional filtering and pagination
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/inventory"Response Status—Response Headers—Response Body—
Request#
Headers:
markupOcp-Apim-Subscription-Key: {your-subscription-key}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | string | No | Search term (searches name, SKU, description, category, tags) |
markup | string | No | Filter by category |
markup | string | No | Filter by tax status: markup , markup , or markup |
markup | string | No | Filter by stock: markup (in stock), markup (out of stock), markup |
markup | number | No | Minimum price filter |
markup | number | No | Maximum price filter |
markup | string | No | Comma-separated tags to filter by |
markup | string | No | Tag match mode: markup (default) or markup |
markup | string | No | Industry pack filter |
markup | string | No | Sort field: markup , markup , markup , markup , markup (default) |
markup | string | No | Sort order: markup or markup (default) |
markup | number | No | Items per page (default: 100, max: 200) |
markup | number | No | Page number (0-indexed, default: 0) |
Example Requests:
# List all products
curl -X GET "https://api.pay.ledger1.ai/portalpay/api/inventory" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"
# Search for "coffee"
curl -X GET "https://api.pay.ledger1.ai/portalpay/api/inventory?q=coffee" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"
# Filter by category and price
curl -X GET "https://api.pay.ledger1.ai/portalpay/api/inventory?category=beverages&priceMin=2&priceMax=10" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"Response#
Success (200 OK):
json{ "items": [ { "id": "inv_abc123", "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "sku": "COFFEE-001", "name": "Espresso", "priceUsd": 3.5, "currency": "USD", "stockQty": 100, "category": "beverages", "description": "Rich espresso shot", "tags": ["hot", "coffee"], "images": ["data:image/jpeg;base64,..."], "attributes": { "size": "single", "roast": "dark" }, "costUsd": 0.75, "taxable": true, "jurisdictionCode": "US-CA", "industryPack": "restaurant", "createdAt": 1698765432000, "updatedAt": 1698765432000 } ], "total": 42, "page": 0, "pageSize": 100 }
Degraded Mode (Cosmos unavailable):
json{ "items": [...], "total": 42, "page": 0, "pageSize": 100, "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/inventory#
Required scopes: inventory:write
Create a new product or update an existing one.
POST/api/inventoryCreate/Update Product
Create a new product or update an existing one
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/inventory" -H "Content-Type: application/json" -d '{ "sku": "COFFEE-001", "name": "Espresso", "priceUsd": 3.5, "stockQty": 100, "category": "beverages", "description": "Rich espresso shot", "tags": [ "hot", "coffee" ], "taxable": true, "costUsd": 0.75 }'Response Status—Response Headers—Response Body—
Request#
Headers:
markupContent-Type: application/json Ocp-Apim-Subscription-Key: {your-subscription-key}
Body Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | string | No | Product ID (for updates) |
markup | string | Yes | Stock keeping unit (unique identifier) |
markup | string | Yes | Product name |
markup | number | Yes | Price in USD (≥ 0) |
markup | number | Yes | Stock quantity (-1 for unlimited, ≥ -1) |
markup | string | No | Currency code (default: USD) |
markup | string | No | Product category |
markup | string | No | Product description |
markup | string[] | No | Product tags (max 24) |
markup | string[] | No | Product images as data URLs (max 3) |
markup | object | No | Custom attributes |
markup | number | No | Cost basis (≥ 0) |
markup | boolean | No | Is taxable (default: false) |
markup | string | No | Tax jurisdiction |
markup | string | No | Industry pack (default: "general") |
Example Requests:
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/inventory" \
-H "Content-Type: application/json" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
-d '{
"sku": "COFFEE-001",
"name": "Espresso",
"priceUsd": 3.5,
"stockQty": 100,
"category": "beverages",
"description": "Rich espresso shot",
"tags": ["hot", "coffee"],
"taxable": true,
"costUsd": 0.75,
"attributes": { "size": "single", "roast": "dark" }
}'Response#
Success (200 OK):
json{ "ok": true, "item": { "id": "inv_abc123", "wallet": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", "sku": "COFFEE-001", "name": "Espresso", "priceUsd": 3.5, "currency": "USD", "stockQty": 100, "category": "beverages", "description": "Rich espresso shot", "tags": ["hot", "coffee"], "taxable": true, "costUsd": 0.75, "attributes": { "size": "single", "roast": "dark" }, "industryPack": "general", "createdAt": 1698765432000, "updatedAt": 1698765432000 } }
Degraded Mode:
json{ "ok": true, "degraded": true, "reason": "cosmos_unavailable", "item": { "...": "..." } }
Special Stock Values#
- markup= Unlimited stock (never runs out)
-1 - markup= Out of stock
0 - markup= Available quantity
> 0
Updating Products#
idExample Update:
curl -X POST "https://api.pay.ledger1.ai/portalpay/api/inventory" \
-H "Content-Type: application/json" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \
-d '{
"id": "inv_abc123",
"sku": "COFFEE-001",
"name": "Espresso (Updated)",
"priceUsd": 3.75,
"stockQty": 150
}'DELETE /portalpay/api/inventory#
Required scopes: inventory:write
Delete a product from inventory.
DELETE/api/inventoryDelete Product
Remove a product from inventory
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 ParametersRequest Bodyapplication/jsonUsing server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.cURLcurl -X DELETE "https://api.pay.ledger1.ai/portalpay/api/inventory"Response Status—Response Headers—Response Body—
Request#
Headers:
markupOcp-Apim-Subscription-Key: {your-subscription-key}
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
markup | string | Yes | Product ID to delete |
Note: Wallet identity is resolved automatically at the gateway based on your subscription; client requests do not include wallet identity.
Example Requests:
curl -X DELETE "https://api.pay.ledger1.ai/portalpay/api/inventory?id=inv_abc123" \
-H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY"Response#
Success (200 OK):
json{ "ok": true }
Degraded Mode:
json{ "ok": true, "degraded": true, "reason": "cosmos_unavailable" }
POST /portalpay/api/inventory/images#
Required scopes: inventory:write
Upload product images (alternative to data URLs).
Request#
Headers:
markupContent-Type: multipart/form-data Ocp-Apim-Subscription-Key: {your-subscription-key}
Form Data:
| Field | Type | Required | Description |
|---|---|---|---|
markup | file | Yes | Image file (JPEG, PNG, WebP) |
markup | string | Yes | Product SKU |
Example:
bashcurl -X POST "https://api.pay.ledger1.ai/portalpay/api/inventory/images" \ -H "Ocp-Apim-Subscription-Key: $APIM_SUBSCRIPTION_KEY" \ -F "image=@product.jpg" \ -F "sku=COFFEE-001"
Response#
Success (200 OK):
json{ "ok": true, "imageUrl": "data:image/jpeg;base64,..." }
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": "invalid_input", "message": "SKU, name, price, and stock quantity are required" }
Common Validation Errors:
- markup– Invalid request parameters
invalid_input - markup– Product ID/SKU doesn't exist
inventory_item_not_found
Best Practices#
SKU Management#
- Use meaningful SKU patterns: markup(e.g.,
CATEGORY-NUMBERmarkup)COFFEE-001 - Keep SKUs short but descriptive
- SKUs are unique per merchant
Stock Management#
typescript// Unlimited stock (services, digital goods) { stockQty: -1 } // Physical goods with inventory { stockQty: 100 } // Out of stock { stockQty: 0 }
Image Best Practices#
- Use square images (1:1 aspect ratio)
- Optimize images before upload (< 500KB)
- Max 3 images per product
- First image is used as thumbnail in orders
Categories#
Suggested categories:
- Restaurant: beverages, appetizers, entrees, desserts, sides
- Retail: clothing, electronics, accessories, home-goods
- Services: consulting, design, development, maintenance
Tags for Search#
json{ "tags": ["vegan", "gluten-free", "organic", "hot", "bestseller"] }
Code Examples#
JavaScript/TypeScript#
typescriptconst APIM_SUBSCRIPTION_KEY = process.env.APIM_SUBSCRIPTION_KEY!; const BASE_URL = 'https://api.pay.ledger1.ai/portalpay'; // Create product async function createProduct(product: any) { const res = await fetch(`${BASE_URL}/api/inventory`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY }, body: JSON.stringify(product) }); const data = await res.json(); if (!data.ok) throw new Error(data.error || 'failed'); return data.item; } // List products async function listProducts(filters: Record<string, any> = {}) { const params = new URLSearchParams(filters); const res = await fetch(`${BASE_URL}/api/inventory?${params}`, { headers: { 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY } }); const data = await res.json(); return data.items; } // Update product async function updateProduct(id: string, updates: any) { return createProduct({ id, ...updates }); } // Delete product async function deleteProduct(id: string) { const res = await fetch(`${BASE_URL}/api/inventory?id=${id}`, { method: 'DELETE', headers: { 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY } }); const data = await res.json(); return data.ok; }
Python#
pythonimport os, requests APIM_SUBSCRIPTION_KEY = os.environ['APIM_SUBSCRIPTION_KEY'] BASE_URL = 'https://api.pay.ledger1.ai/portalpay' def create_product(product): r = requests.post( f'{BASE_URL}/api/inventory', headers={'Content-Type': 'application/json', 'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY}, json=product ) data = r.json() if not data.get('ok'): raise Exception(data.get('error') or 'failed') return data['item'] def list_products(**filters): r = requests.get( f'{BASE_URL}/api/inventory', headers={'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY}, params=filters ) return r.json().get('items', []) def update_product(id, **updates): return create_product({'id': id, **updates}) def delete_product(id): r = requests.delete( f'{BASE_URL}/api/inventory', headers={'Ocp-Apim-Subscription-Key': APIM_SUBSCRIPTION_KEY}, params={'id': id} ) return r.json().get('ok', False)
Notes on Auth Models#
- Developer integrations must use markup. Wallet identity is resolved automatically at the gateway based on your subscription; the backend trusts the resolved identity.
Ocp-Apim-Subscription-Key - Admin/UI operations performed in the PortalPay app use JWT cookies (markup) with CSRF and role checks; no subscription key is required for in-app admin flows.
cb_auth_token - Client requests do not include wallet identity; APIM strips wallet headers and stamps the resolved identity.
