Marc’s Inventory API
This document describes authentication, headers, pagination, common responses, and every major endpoint exposed by the backend. Admin endpoints are login‑gated and must be scoped to an account.
Quickstart
1) Login to obtain a bearer token. 2) Call APIs with Authorization: Bearer <token> and tenant scoping via X-Account-ID or ?account_id=.
# 1) Login (form-encoded)
curl -X POST https://api.example.com/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=marc@example.com&password=secret&account_id=123"
# => { "access_token": "...", "token_type":"bearer", "expires_in":3600 }
# 2) List products for account 123
curl "https://api.example.com/catalog/products?account_id=123&limit=25" \
-H "Authorization: Bearer <token>"
Base URL & Versioning
- Base URL: depends on your deployment (e.g.,
https://api.example.com). - Versioning: current APIs are unversioned. A media version header may be introduced later (e.g.,
Accept: application/json;version=1).
Authentication
Most admin routes require a bearer token obtained via login. On multi‑tenant systems, include an account scope.
| Method | Path | Description |
|---|---|---|
| POST | /auth/login | Form: username, password. Optional ?account_id= to scope login. |
| GET | /auth/me | Returns the current user and memberships. |
| POST | /auth/refresh | (If enabled) exchange a refresh token for access token. |
Response (login)
200 OK
{ "access_token":"<jwt>", "token_type":"bearer", "expires_in":3600 }
Tenant Scoping
Include the tenant scope for account‑specific operations using either:
- Header:
X-Account-ID: 123 - or Query:
?account_id=123
Headers & Content Types
Authorization: Bearer <token>Content-Type: application/json(unless otherwise specified, e.g., login usesapplication/x-www-form-urlencoded)Accept: application/json- Optional idempotency:
X-Idempotency-Key: uuidon POST/PUT to safely retry.
Pagination
Endpoints that list resources (e.g., /catalog/products) support cursor‑based pagination.
limit: page size (default 25, max 100)cursor: opaque cursor returned by previous call
GET /catalog/products?account_id=123&limit=25
{
"items":[ ... ],
"next_cursor":"eyJvZmZzZXQiOjI1LCJpZCI6MTAwfQ=="
}
Errors
Errors use standard HTTP codes with a consistent JSON body.
| Status | Meaning | Example |
|---|---|---|
| 400 | Bad request / validation | {"detail":"Invalid parameter 'limit'"} |
| 401 | Unauthorized | {"detail":"Not authenticated"} |
| 403 | Forbidden | {"detail":"Insufficient role"} |
| 404 | Not found | {"detail":"Resource not found"} |
| 409 | Conflict | {"detail":"Already exists"} |
| 422 | Unprocessable entity | {"detail":[{"loc":["body","field"],"msg":"required"}]} |
| 429 | Too many requests | {"detail":"Rate limit exceeded"} |
| 500 | Server error | {"detail":"Internal server error"} |
Catalog
Public product listing for rendering the store’s catalog or internal stock views.
| Method | Path | Notes |
|---|---|---|
| GET | /catalog/products | Filter with q, category; supports limit, cursor. |
Request
GET /catalog/products?account_id=123&q=ball&category=Accessories&limit=25
Response
200 OK
{
"items":[
{"id":101,"sku":"BALL-5","name":"Size 5 Match Ball","stock_qty":42,"category":"Balls"},
{"id":102,"sku":"SHIN-GUARD","name":"Shin Guards Small","stock_qty":12,"category":"Protection"}
],
"next_cursor":null,
"category_facets":{"Balls":18,"Protection":9}
}
Inventory & Orders
| Method | Path | Description |
|---|---|---|
| POST | /admin/inventory/adjust | Create an adjustment movement (positive or negative). |
| POST | /admin/ops/reapply-inventory?order_id= | Reapply sale movements for a given order. |
Adjust Inventory — Request
POST /admin/inventory/adjust
Headers:
Authorization: Bearer <token>
X-Account-ID: 123
Body:
{
"product_id": 101,
"delta": -3,
"reason": "stocktake" // one of: manual_adjust | stocktake | damage | return
}
Response
200 OK
{
"ok": true,
"movement_id": 9876,
"product_id": 101,
"new_stock_qty": 39
}
Emails
| Method | Path | Description |
|---|---|---|
| GET | /admin/emails/recipients | List recipients grouped by purpose. |
| PUT | /admin/emails/recipients | Replace recipients for a purpose. |
| GET | /admin/emails/templates | List account/global templates. |
| GET | /admin/emails/templates/{key} | Fetch one template (account fallback to global). |
| PUT | /admin/emails/templates/{key} | Upsert a template. |
| GET | /admin/emails-ui | Web UI for templates (requires login). |
Recipients — PUT Body
{
"purpose":"ops",
"emails":["ops@example.com","mgr@example.com"]
}
Template Upsert — PUT Body
{
"subject":"Your order has shipped",
"html":"<p>Hello {{ name }}, your order is on the way.</p>",
"text":"Hello {{ name }}, your order is on the way.",
"enabled": true,
"global_template": false
}
Shipping
| Method | Path | Description |
|---|---|---|
| POST | /admin/shipping/label | Create a label from order data or explicit addresses. |
| POST | /admin/shipping/verify-address | Standardize/validate addresses. |
Create Label — Request
POST /admin/shipping/label
Headers:
Authorization: Bearer <token>
X-Account-ID: 123
Body:
{
"order_id": 555, // optional
"from_address": { "name":"Shop A", "street1":"123 Main", "city":"Austin","state":"TX","zip":"78701","country":"US" },
"to_address": { "name":"Alex", "street1":"77 Park Ave", "city":"New York","state":"NY","zip":"10016","country":"US" },
"parcels":[{"length":"10","width":"10","height":"10","distance_unit":"in","weight":"2","mass_unit":"lb"}],
"label_file_type":"PDF"
}
Response
200 OK
{
"shipment_id":"shp_...",
"rate_id":"rate_...",
"label_url":"https://.../label.pdf",
"tracking_number":"1Z..."
}
Integrations
Secret names/ARNs are stored per account. Supported provider keys: quickbooks, stripe, shippo.
| Method | Path | Description |
|---|---|---|
| GET | /admin/integrations/accounts/{account_id}/{provider}/config | Read current secret name. |
| PUT | /admin/integrations/accounts/{account_id}/{provider}/config | Set secret name (validated exists). |
| DELETE | /admin/integrations/accounts/{account_id}/{provider}/config | Clear secret name. |
| POST | /admin/integrations/email/provision-templates | Install/refresh default SES templates. |
| GET | /admin/quickbooks/connect-url | Return OAuth2 authorization URL. |
PUT Body
{ "secret_name":"arn:aws:secretsmanager:...:secret:quickbooks/prod" }
Payments
| Method | Path | Description |
|---|---|---|
| POST | /admin/payments/intent | Create a payment intent (e.g., Stripe) for an amount. |
Request
POST /admin/payments/intent
{
"account_id":123,
"amount_cents": 4999,
"currency": "USD",
"metadata": {"order_id":555}
}
Response
200 OK
{ "id":"pi_...", "client_secret":"...", "status":"requires_confirmation" }
Feature Flags
| Method | Path | Description |
|---|---|---|
| GET | /admin/flags | List flags (account and global). |
| GET | /admin/flags/{key} | Fetch one flag with fallback. |
| PUT | /admin/flags/{key} | Upsert flag (set scope to account or global). |
| DELETE | /admin/flags/{key} | Soft delete account flag. |
PUT Body
{
"value": true,
"scope": "account" // or "global"
}
Webhooks
The system can receive incoming events (e.g., Stripe). Webhooks must be signed and verified.
| Method | Path | Description |
|---|---|---|
| POST | /stripe/webhook | Stripe events (e.g., payment_intent.succeeded). Verify signature with your endpoint secret. |
Example event
POST /stripe/webhook
Headers:
Stripe-Signature: t=...,v1=...,v0=...
Body:
{
"id": "evt_...",
"type": "payment_intent.succeeded",
"data": { "object": { "id":"pi_...", "amount":4999, "currency":"usd" } }
}
Questions? See the landing page for contact options, or open the Admin page and use the built‑in UI to exercise endpoints after login.