An investment in knowledge pays the best interest.
Benjamin Franklin

Inventory

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
    cb_auth_token
    ) and CSRF protections. See
    markup
    ../auth.md
    for details.

GET /portalpay/api/inventory#

Required scopes: inventory:read

List all products in your inventory with advanced filtering, sorting, and pagination.

GET/api/inventory

List 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 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/inventory"
Response Status
Response Headers
Response Body

Request#

Headers:

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

Query Parameters:

ParameterTypeRequiredDescription
markup
q
stringNoSearch term (searches name, SKU, description, category, tags)
markup
category
stringNoFilter by category
markup
taxable
stringNoFilter by tax status:
markup
true
,
markup
false
, or
markup
any
markup
stock
stringNoFilter by stock:
markup
in
(in stock),
markup
out
(out of stock),
markup
any
markup
priceMin
numberNoMinimum price filter
markup
priceMax
numberNoMaximum price filter
markup
tags
stringNoComma-separated tags to filter by
markup
tagsMode
stringNoTag match mode:
markup
any
(default) or
markup
all
markup
pack
stringNoIndustry pack filter
markup
sort
stringNoSort field:
markup
name
,
markup
sku
,
markup
priceUsd
,
markup
stockQty
,
markup
updatedAt
(default)
markup
order
stringNoSort order:
markup
asc
or
markup
desc
(default)
markup
limit
numberNoItems per page (default: 100, max: 200)
markup
page
numberNoPage 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/inventory

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

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

Body Parameters:

ParameterTypeRequiredDescription
markup
id
stringNoProduct ID (for updates)
markup
sku
stringYesStock keeping unit (unique identifier)
markup
name
stringYesProduct name
markup
priceUsd
numberYesPrice in USD (≥ 0)
markup
stockQty
numberYesStock quantity (-1 for unlimited, ≥ -1)
markup
currency
stringNoCurrency code (default: USD)
markup
category
stringNoProduct category
markup
description
stringNoProduct description
markup
tags
string[]NoProduct tags (max 24)
markup
images
string[]NoProduct images as data URLs (max 3)
markup
attributes
objectNoCustom attributes
markup
costUsd
numberNoCost basis (≥ 0)
markup
taxable
booleanNoIs taxable (default: false)
markup
jurisdictionCode
stringNoTax jurisdiction
markup
industryPack
stringNoIndustry 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
    -1
    = Unlimited stock (never runs out)
  • markup
    0
    = Out of stock
  • markup
    > 0
    = Available quantity

Updating Products#

To update a product, include its
markup
id
in the request body. The system is idempotent by SKU, so POST with the same SKU updates the existing product.

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

Delete 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 Parameters
Request Body
application/json
Using server-side proxy to avoid CORS. Requests go through /api/tryit-proxy to AFD/APIM.
cURL
curl -X DELETE "https://api.pay.ledger1.ai/portalpay/api/inventory"
Response Status
Response Headers
Response Body

Request#

Headers:

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

Query Parameters:

ParameterTypeRequiredDescription
markup
id
stringYesProduct 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:

markup
Content-Type: multipart/form-data
Ocp-Apim-Subscription-Key: {your-subscription-key}

Form Data:

FieldTypeRequiredDescription
markup
image
fileYesImage file (JPEG, PNG, WebP)
markup
sku
stringYesProduct SKU

Example:

bash
curl -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_input
    – Invalid request parameters
  • markup
    inventory_item_not_found
    – Product ID/SKU doesn't exist

Best Practices#

SKU Management#

  • Use meaningful SKU patterns:
    markup
    CATEGORY-NUMBER
    (e.g.,
    markup
    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
json
{ "tags": ["vegan", "gluten-free", "organic", "hot", "bestseller"] }

Code Examples#

JavaScript/TypeScript#

typescript
const 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#

python
import 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
    Ocp-Apim-Subscription-Key
    . Wallet identity is resolved automatically at the gateway based on your subscription; the backend trusts the resolved identity.
  • Admin/UI operations performed in the PortalPay app use JWT cookies (
    markup
    cb_auth_token
    ) with CSRF and role checks; no subscription key is required for in-app admin flows.
  • Client requests do not include wallet identity; APIM strips wallet headers and stamps the resolved identity.