Skip to content

REST API

All routes are mounted under the /api prefix. The full interactive spec is available at GET /api/reference (Swagger UI) and GET /api/doc (OpenAPI 3.0 JSON).

Authentication: API key via x-api-key header, or a Better Auth session cookie for browser clients.

Successful responses: { data: ... }. List endpoints include meta.pagination. Errors return { error: { code, message } }.

X-Request-Id: Every response includes this header with a unique request identifier.


| Method | Path | Description | |--------|------|-------------| | GET | /api/doc | OpenAPI 3.0 JSON spec | | GET | /api/reference | Swagger UI (interactive explorer) |

All core routes and plugin routes registered via router() appear in the spec automatically.


| Method | Path | Description | |--------|------|-------------| | GET | /api/health | Server status, version, database connectivity |

Probes the database connection. Returns 503 if the database is unreachable.

// healthy
{ "status": "ok", "version": "0.1.0" }
// unhealthy
{ "status": "error", "version": "0.1.0", "database": "unreachable" }

Body size: 1 MB maximum. Requests exceeding this receive 413 Payload Too Large.

Rate limiting: Returns 429 Too Many Requests when exceeded.

Rate limit headers on every response:

| Header | Description | |--------|-------------| | X-RateLimit-Limit | Max requests in the current window | | X-RateLimit-Remaining | Remaining requests in this window | | X-RateLimit-Reset | Unix timestamp when the window resets | | Retry-After | Seconds until retry (only on 429) |

CSRF: Mutation methods (POST, PUT, PATCH, DELETE) validate the Origin header for browser requests. API-key requests are exempt.


Base path: /api/catalog

| Method | Path | Description | |--------|------|-------------| | GET | /catalog/entities | List catalog entities | | GET | /catalog/entities/:idOrSlug | Get entity by UUID or slug | | POST | /catalog/entities | Create a catalog entity | | PATCH | /catalog/entities/:id | Update an entity | | DELETE | /catalog/entities/:id | Delete an entity | | POST | /catalog/entities/:id/publish | Transition to active status | | POST | /catalog/entities/:id/archive | Transition to archived status | | POST | /catalog/entities/:id/discontinue | Transition to discontinued status |

GET /catalog/entities query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | type | string | Filter by entity type (product, digitalDownload, etc.) | | status | string | Filter by status (draft, active, archived, discontinued) | | category | string | Filter by category ID | | brand | string | Filter by brand ID | | page | number | Page number (default 1) | | limit | number | Items per page (default 20, max 100) | | sort | string | Sort field and direction (e.g. createdAt:desc) | | include | string | Comma-separated: attributes, variants, categories, brands, media, inventory, pricing |

POST /catalog/entities body:

{
"type": "product",
"slug": "blue-widget",
"status": "draft",
"metadata": {}
}

| Method | Path | Description | |--------|------|-------------| | PUT | /catalog/entities/:id/attributes/:locale | Set localized attributes | | GET | /catalog/entities/:id/attributes/:locale | Get localized attributes |

PUT body:

{
"title": "Blue Widget",
"description": "A fine blue widget.",
"seoTitle": "Buy Blue Widget",
"seoDescription": "Best blue widget online."
}

| Method | Path | Description | |--------|------|-------------| | GET | /catalog/categories | List all categories | | POST | /catalog/categories | Create a category | | PATCH | /catalog/categories/:categoryId | Update a category | | DELETE | /catalog/categories/:categoryId | Delete a category | | POST | /catalog/entities/:id/categories/:categoryId | Add entity to a category | | DELETE | /catalog/entities/:id/categories/:categoryId | Remove entity from a category |

| Method | Path | Description | |--------|------|-------------| | GET | /catalog/brands | List all brands | | POST | /catalog/brands | Create a brand | | PATCH | /catalog/brands/:brandId | Update a brand | | DELETE | /catalog/brands/:brandId | Delete a brand | | POST | /catalog/entities/:id/brands/:brandId | Add entity to a brand | | DELETE | /catalog/entities/:id/brands/:brandId | Remove entity from a brand |

| Method | Path | Description | |--------|------|-------------| | POST | /catalog/entities/:id/options | Create an option type | | POST | /catalog/options/:optionTypeId/values | Create an option value | | POST | /catalog/entities/:id/variants | Create a single variant | | POST | /catalog/entities/:id/variants/generate | Generate variants from an option matrix | | POST | /catalog/entities/:id/variants/quick | One-call variant: upsert option axes inline + seed zero-stock (201 created / 200 existing) | | POST | /catalog/entities/:id/variants/bulk | Create the full cartesian matrix from axes, skipping existing combos |


Base path: /api/carts

| Method | Path | Description | |--------|------|-------------| | POST | /carts | Create a new cart | | GET | /carts/:id | Get cart with line items | | POST | /carts/:id/items | Add an item to the cart | | PATCH | /carts/:id/items/:itemId | Update item quantity | | DELETE | /carts/:id/items/:itemId | Remove an item from the cart |

POST /carts body:

{
"currency": "USD",
"customerId": "optional-uuid"
}

POST /carts/:id/items body:

{
"entityId": "uuid",
"variantId": "uuid-or-omit",
"quantity": 2,
"unitPriceSnapshot": 1999
}

PATCH /carts/:id/items/:itemId body:

{ "quantity": 3 }

| Method | Path | Description | |--------|------|-------------| | POST | /api/checkout | Create an order from a cart |

Body:

{
"cartId": "uuid",
"paymentMethodId": "pm_...",
"currency": "USD",
"customerId": "optional",
"customerGroupIds": ["optional"],
"promotionCodes": ["SAVE10"],
"shippingAddress": {
"line1": "123 Main St",
"city": "Springfield",
"state": "IL",
"postalCode": "62701",
"country": "US"
}
}

Returns the created order. If after-hooks produce non-fatal errors, the response includes meta.hookErrors.

Failure: 422 with { error: { code: "CHECKOUT_FAILED", message: "..." } }.


Base path: /api/orders

| Method | Path | Description | |--------|------|-------------| | GET | /orders | List orders | | GET | /orders/:idOrNumber | Get order by UUID or order number (ORD-YYYY-NNNNNN) | | PATCH | /orders/:id/status | Change order status | | GET | /orders/:id/fulfillments | List fulfillments for an order | | POST | /orders/:id/refunds | Refund specific line quantities (orders:update) | | POST | /orders/:id/refunds/:refundId/undo | Undo a refund within the window (orders:update) | | GET | /orders/:id/refunds | List an order’s refunds (orders:read) | | GET | /orders/refunds/cap | Acting operator’s daily refund-cap status (orders:read) | | POST | /orders/:id/notes | Add a note (orders:update) | | GET | /orders/:id/notes | List notes, pinned-first (orders:read) | | DELETE | /orders/:id/notes/:noteId | Delete a note (orders:update) | | GET | /orders/:id/timeline | Merged activity timeline (orders:read) | | GET | /orders/:id/invoice.pdf | Fiscal PDF invoice (orders:read) | | GET | /orders/:id/invoice.html | HTML invoice (orders:read) | | GET | /orders/:id/receipt.html | HTML receipt (orders:read) | | POST | /orders/:id/invoice/email | Email the invoice (orders:read) |

GET /orders query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | status | string | Filter by status | | page | number | Page number | | limit | number | Items per page |

PATCH /orders/:id/status body:

{
"status": "confirmed",
"reason": "optional reason string"
}

Valid transitions: pending → confirmed → processing → partially_fulfilled | fulfilled → refunded. Cancel is allowed from pending, confirmed, processing, partially_fulfilled.

POST /orders/:id/refunds body:

{
"lines": [{ "lineItemId": "uuid", "quantity": 1 }],
"reason": "optional, max 500 chars"
}

Rejects refunding more than quantity - refundedQuantity on a line (422). A per-operator daily cap (policies.refundDailyCap, minor units) returns 403 when exceeded. Undo is blocked past policies.refundUndoWindowMinutes (default 15). See Refunds & Exchanges.

POST /orders/:id/notes body:

{ "body": "note text (1–2000 chars)", "pinned": false }

GET /orders/:id/timeline returns newest-first entries merging status changes, notes, and refunds (with undo events):

{ "data": [{ "type": "status | note | refund", "at": "ISO", "actor": "...", "summary": "...", "data": {} }] }

Base path: /api/inventory

| Method | Path | Description | |--------|------|-------------| | GET | /inventory/check | Check stock for one or more entities | | POST | /inventory/adjust | Adjust an inventory level | | POST | /inventory/reserve | Reserve inventory | | POST | /inventory/release | Release a reservation | | POST | /inventory/warehouses | Create a warehouse | | GET | /inventory/warehouses | List warehouses |

GET /inventory/check query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | entityIds | string | Comma-separated entity UUIDs |

POST /inventory/adjust body:

{
"entityId": "uuid",
"variantId": "optional-uuid",
"warehouseId": "uuid",
"type": "restock",
"quantity": 50
}

Base path: /api/pricing

| Method | Path | Description | |--------|------|-------------| | POST | /pricing/prices | Create a price record | | GET | /pricing/prices | List prices | | POST | /pricing/modifiers | Create a price modifier |

GET /pricing/prices query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | entityId | string | Filter by entity ID | | variantId | string | Filter by variant ID | | currency | string | Filter by currency code | | customerGroupId | string | Filter by customer group |

POST /pricing/prices body:

{
"entityId": "uuid",
"variantId": "optional-uuid",
"currency": "USD",
"amount": 2999
}

Base path: /api/promotions

| Method | Path | Description | |--------|------|-------------| | POST | /promotions | Create a promotion | | GET | /promotions | List active promotions | | POST | /promotions/validate | Validate a promotion code against a cart | | POST | /promotions/:id/deactivate | Deactivate a promotion |

POST /promotions body:

{
"code": "SAVE10",
"name": "Save 10%",
"type": "percentage_off_order",
"value": 10,
"validFrom": "2025-01-01T00:00:00Z",
"validUntil": "2025-12-31T23:59:59Z"
}

validFrom and validUntil are ISO 8601 strings coerced to Date objects on the server.

POST /promotions/validate body:

{
"code": "SAVE10",
"currency": "USD",
"subtotal": 5000,
"lineItems": [
{
"entityId": "uuid",
"entityType": "product",
"quantity": 1,
"unitPrice": 5000,
"totalPrice": 5000
}
],
"customerId": "optional",
"customerGroupIds": ["optional"]
}

Base path: /api/search

| Method | Path | Description | |--------|------|-------------| | GET | /search | Full-text search across catalog entities | | GET | /search/suggest | Autocomplete prefix suggestions |

GET /search query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | q | string | Search query | | type | string | Filter by entity type | | category | string | Filter by category | | brand | string | Filter by brand | | status | string | Filter by status | | page | number | Page number (default 1) | | limit | number | Results per page (default 20, max 100) | | facets | string | Comma-separated facet fields to return |

Response includes data (hits array) and meta with total, page, limit, facets.

GET /search/suggest query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | prefix | string | Prefix to match | | type | string | Filter by entity type | | limit | number | Max suggestions (default 10) |


Base path: /api/media

| Method | Path | Description | |--------|------|-------------| | POST | /media/upload | Upload a file (multipart/form-data) | | GET | /media/:id | Get media URL (redirects 302) | | DELETE | /media/:id | Delete a media asset | | POST | /media/attach | Attach a media asset to an entity |

POST /media/upload fields (multipart/form-data):

| Field | Type | Required | |-------|------|----------| | file | File | Yes | | alt | string | No |

GET /media/:id query parameters:

| Parameter | Value | Description | |-----------|-------|-------------| | signed | "true" | Return a signed URL with expiry instead of a public URL |

POST /media/attach body:

{
"entityId": "uuid",
"mediaAssetId": "uuid",
"role": "thumbnail"
}

| Method | Path | Description | |--------|------|-------------| | POST | /api/payments/webhook | Payment provider webhook receiver |

Accepts raw webhook payloads from the configured payment adapter. On payment_intent.succeeded with metadata.orderId, transitions the order to confirmed automatically.


Base path: /api/webhooks

Requires webhooks:manage permission.

| Method | Path | Description | |--------|------|-------------| | POST | /webhooks | Register a webhook endpoint | | GET | /webhooks | List registered endpoints | | DELETE | /webhooks/:id | Delete a webhook endpoint |


Base path: /api/me

Requires an authenticated Better Auth session cookie. All routes return 401 without a session.

| Method | Path | Description | |--------|------|-------------| | GET | /me/profile | Get current customer profile | | PATCH | /me/profile | Update current customer profile | | GET | /me/addresses | List customer addresses | | POST | /me/addresses | Add a new address | | DELETE | /me/addresses/:id | Delete an address | | GET | /me/orders | List orders for the current customer | | GET | /me/orders/:idOrNumber | Get a specific order by UUID or order number | | GET | /me/orders/:idOrNumber/tracking | Get fulfillment tracking for an order | | GET | /me/orders/:orderId/downloads | Get download links for digital items | | POST | /me/orders/:orderId/reorder | Create a new cart pre-filled from a previous order |

GET /me/orders query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | status | string | Filter by order status | | page | number | Page number | | limit | number | Items per page |


Base path: /api/analytics

| Method | Path | Description | |--------|------|-------------| | GET | /analytics/meta | List available models, measures, and dimensions | | POST | /analytics/query | Execute an analytics query | | GET | /analytics/reports | List canned retail reports (analytics:read) | | GET | /analytics/reports/:name | Run a retail report (analytics:read) |

POST /analytics/query body: See Analytics reference for the full query format.

Retail reports (:name): daily-journal, tax-summary, inventory-aging, sell-through, reorder-needed, staff-sales. Query params (all YYYY-MM-DD, optional): date for single-day reports; from + to for ranges. Calendar math uses the store’s settings.general.timezone. See Retail reports.


Base path: /api/settings. All routes require settings:manage. See Store Settings.

| Method | Path | Description | |--------|------|-------------| | GET | /settings | All groups | | GET | /settings/:group | One group’s value ({} when unset) | | PATCH | /settings/:group | Shallow-merge a group; a null value deletes that key |

Known groups are validated (422 on bad input): general (currency, timezone, locale), branding (store identity for receipts), policies (e.g. refundDailyCap, refundUndoWindowMinutes). Unknown groups accept any object.


Base path: /api/tax. Class routes require tax:manage. See Tax Classes.

| Method | Path | Description | |--------|------|-------------| | GET | /tax/classes | List tax classes | | POST | /tax/classes | Create a class (name slug, rateBps, isDefault?, isActive?) | | PATCH | /tax/classes/:id | Update a class | | DELETE | /tax/classes/:id | Delete a class |

Per-line tax resolves variant taxClass → entity taxClass → the org’s default class → 0. When an org defines active classes they take precedence over runtime region rates.


Base path: /api/audit

Requires audit:read permission.

| Method | Path | Description | |--------|------|-------------| | GET | /audit | List audit entries | | GET | /audit/:entityType/:entityId | Audit history for a specific entity |

GET /audit query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | entityType | string | Filter by entity type (e.g. order, catalog_entity, inventory) | | entityId | string | Filter by entity UUID | | event | string | Filter by event name (e.g. orders.statusChange) | | actorId | string | Filter by actor | | from | string | ISO 8601 start date | | to | string | ISO 8601 end date | | limit | number | Max entries (default 50, max 100) |


Base path: /api/auth

Authentication is handled by Better Auth. These are the core sign-up/sign-in endpoints. Better Auth exposes additional endpoints for password reset, email verification, sessions, and OAuth.

| Method | Path | Description | |--------|------|-------------| | POST | /auth/sign-up/email | Register a new user | | POST | /auth/sign-in/email | Sign in with email and password | | GET | /auth/get-session | Get the current session |

POST /auth/sign-up/email body:

{
"email": "user@example.com",
"password": "secret",
"name": "Jane Doe"
}

POST /auth/sign-in/email body:

{
"email": "user@example.com",
"password": "secret"
}

Base path: /api/admin/jobs

Requires jobs:admin permission.

| Method | Path | Description | |--------|------|-------------| | GET | /admin/jobs/failed | List failed background jobs | | POST | /admin/jobs/:id/retry | Re-enqueue a failed job |

GET /admin/jobs/failed query parameters:

| Parameter | Type | Description | |-----------|------|-------------| | page | number | Page number | | limit | number | Items per page |

POST /admin/jobs/:id/retry returns the updated job record.

The cron job runner endpoint:

| Method | Path | Description | |--------|------|-------------| | GET | /api/jobs/run | Process pending jobs (cron target) |

Query parameters: queue (default "default"), limit (default 10). Requires *:* permission via x-api-key.


Added by @porulle/plugin-marketplace. All paths prefixed with /api/marketplace/.

| Method | Path | Description | |--------|------|-------------| | GET | /vendors | List vendors | | POST | /vendors | Register a vendor | | GET | /vendors/:id | Vendor detail | | PATCH | /vendors/:id | Update vendor | | POST | /vendors/:id/approve | Approve vendor | | POST | /vendors/:id/reject | Reject with reason | | POST | /vendors/:id/suspend | Suspend vendor | | POST | /vendors/:id/reinstate | Reinstate vendor | | GET | /vendors/:id/documents | List documents | | POST | /vendors/:id/documents | Upload document | | POST | /vendors/:id/documents/:docId/approve | Approve document | | POST | /vendors/:id/documents/:docId/reject | Reject document | | GET | /vendors/:id/balance | Balance ledger | | GET | /vendors/:id/performance | Performance metrics and rating |

Scoped automatically to actor.vendorId.

| Method | Path | Description | |--------|------|-------------| | GET | /vendor/me | Own vendor profile | | PATCH | /vendor/me | Update own profile | | GET | /vendor/me/orders | Own sub-orders | | POST | /vendor/me/orders/:id/confirm | Confirm sub-order | | POST | /vendor/me/orders/:id/ship | Ship with tracking number | | POST | /vendor/me/orders/:id/deliver | Mark delivered | | POST | /vendor/me/orders/:id/cancel | Cancel sub-order | | GET | /vendor/me/payouts | Payout history | | GET | /vendor/me/balance | Balance ledger | | GET | /vendor/me/reviews | Reviews received |

| Group | Paths | |-------|-------| | Commission rules | GET /commission-rules, POST /commission-rules, PATCH /commission-rules/:id, DELETE /commission-rules/:id | | Payouts | GET /payouts, POST /payouts/run, POST /payouts/:id/retry | | Sub-orders (admin) | GET /sub-orders, GET /sub-orders/:id, PATCH /sub-orders/:id/status | | Disputes | POST /disputes, GET /disputes, POST /disputes/:id/respond, POST /disputes/:id/escalate, POST /disputes/:id/resolve | | Returns | POST /returns, POST /returns/:id/ship-back, POST /returns/:id/receive | | Reviews | POST /vendors/:id/reviews, GET /vendors/:id/reviews, PATCH /vendors/:id/reviews/:reviewId |


Added by @porulle/plugin-pos. See Point of Sale and Refunds & Exchanges. Service-level failures on these routes surface as 500 with the message.

| Method | Path | Description | |--------|------|-------------| | PUT | /api/pos/auth/pin | Set/rotate an operator PIN (pos:admin) | | POST | /api/pos/auth/pin-login | PIN login → mints a short-lived per-shift API key (pos:operate) | | POST | /api/pos/auth/override | Manager override by PIN (pos:operate) | | POST | /api/pos/exchanges | Return + replacement order in one transaction (pos:manage + orders:update + orders:create) |


Added by @porulle/plugin-layaway. All paths under /api/layaways. See Layaway.

| Method | Path | Description | |--------|------|-------------| | POST | /layaways | Create a plan; reserves stock (layaway:operate) | | GET | /layaways | List plans, optional ?status (layaway:operate) | | GET | /layaways/:id | Plan with its payment ledger (layaway:operate) | | POST | /layaways/:id/payments | Record an installment; auto-completes at full payment (layaway:operate + orders:create) | | POST | /layaways/:id/forfeit | Forfeit a plan; releases stock (layaway:manage) |


| Endpoint group | Required permission | |----------------|---------------------| | POST /webhooks, GET /webhooks, DELETE /webhooks/:id | webhooks:manage | | GET /audit, GET /audit/:entityType/:entityId | audit:read | | POST /pricing/prices, POST /pricing/modifiers | pricing:create | | GET /pricing/prices | pricing:update | | POST /promotions | promotions:create | | PATCH /promotions/:id, POST /promotions/:id/deactivate | promotions:update | | POST /marketplace/vendors/:id/approve, .../suspend | marketplace:admin | | GET /admin/jobs/failed, POST /admin/jobs/:id/retry | jobs:admin | | GET /settings, PATCH /settings/:group | settings:manage | | GET /analytics/reports, GET /analytics/reports/:name | analytics:read | | GET/POST/PATCH/DELETE /tax/classes | tax:manage | | POST /orders/:id/refunds, .../undo, notes write | orders:update | | POST /pos/auth/pin | pos:admin | | POST /pos/auth/pin-login, .../override | pos:operate | | POST /pos/exchanges | pos:manage (+ orders:update, orders:create) | | POST /layaways, .../payments | layaway:operate | | POST /layaways/:id/forfeit | layaway:manage |


Events that trigger delivery when a matching endpoint is registered:

| Event | Trigger | |-------|---------| | orders.create | New order created via checkout | | orders.statusChange | Order transitions to a new status | | catalog.create | New catalog entity created | | catalog.update | Catalog entity updated | | catalog.delete | Catalog entity deleted | | inventory.afterAdjust | Inventory level adjusted | | customers.create | New customer profile created | | customers.update | Customer profile updated | | pricing.create | New price record created | | pricing.update | Price record updated | | promotions.create | New promotion created | | promotions.update | Promotion updated or deactivated | | fulfillment.create | New fulfillment record created | | cart.afterAddItem | Item added to a cart |


{
"error": {
"code": "NOT_FOUND",
"message": "Entity not found."
}
}

| Code | HTTP status | Description | |------|-------------|-------------| | NOT_FOUND | 404 | Resource does not exist | | VALIDATION_FAILED | 422 | Input validation failed | | CHECKOUT_FAILED | 422 | Checkout pipeline error | | FORBIDDEN | 403 | Insufficient permissions | | CONFLICT | 409 | Conflicting state (e.g., duplicate slug) | | TOO_MANY_REQUESTS | 429 | Rate limit exceeded | | PAYLOAD_TOO_LARGE | 413 | Request body exceeds 1 MB |


All list endpoints accept page and limit query parameters. limit is capped at 100.

{
"data": [],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total": 142,
"totalPages": 8
}
}
}