@adocommercekit/admin-api publishes 42 host-owned endpoints under /api/commerce/admin, plus the admin service layer both admin surfaces share. A headless host can stop here: everything the Inertia panel does is reachable through this contract.
pnpm add @adocommercekit/admin-api @adonisjs/bouncer
node ace configure @adocommercekit/admin-api
node ace migration:run
node ace commerce:doctorconfigure() aborts unless @adocommercekit/core is already configured, then publishes the controllers, validators, middleware, policy tree, routes, and the audit-table migration into your application and records an upgrade baseline for each file. Nothing runs from the package at request time.
WARNING — Every ability denies by default
The published policy stubs grant nothing. Implement
commerceAdminRoles()inapp/policies/commerce/main.tsbefore an operator can do anything; until you do, every endpoint answers403.commerce:doctorreports the unmapped state as a failure. See Abilities and policies.
Conventions
- Every request is authenticated. There is no anonymous admin route, metrics included; an unauthenticated request answers
401 E_UNAUTHORIZED. - Every route names exactly one ability from the frozen catalog and checks it before the controller body runs. A denial is
403 E_COMMERCE_FORBIDDENwith the ability name indetails.ability— ability names are vocabulary, not secrets. - Shield/CSRF stays on. No admin route is exempt.
- Responses are JSON. Money renders as
{ amount, currency, formatted, amountMinor };amountMinoris the stringified integer minor-unit value, so charts and exports never parse a formatted string. - Everything serialized here goes through a transformer’s
forAdminvariant. Provider credentials, encrypted columns, raw provider payloads, and customer cart tokens never appear. - Rate limits are per resolved actor: mutations 60/min, metrics 120/min, other reads 240/min. Every response carries
X-RateLimit-*. - Validation failures answer
422with the commerce envelope anddetails.messages, never a redirect.
{
"error": {
"code": "E_COMMERCE_FORBIDDEN",
"message": "Missing ability commerce.payments.refund",
"details": { "ability": "commerce.payments.refund" }
}
}Branch on error.code. Codes specific to this surface:
| Code | Status | Meaning |
|---|---|---|
E_UNAUTHORIZED |
401 | No authenticated actor. |
E_COMMERCE_FORBIDDEN |
403 | The actor lacks the endpoint’s ability. |
E_COMMERCE_INVALID_TRANSITION |
409 | The state machine refused; details.currentState carries the state the surface observed. |
E_IDEMPOTENCY_CONFLICT |
409 | An Idempotency-Key was reused with a different payload. |
E_VALIDATION_ERROR |
422 | The payload, or a value a service rejected, is invalid. |
Lists
Every index endpoint is keyset paginated and takes the same shape:
| Query | Default | Constraint |
|---|---|---|
limit |
25 |
Integer from 1 through 100 |
cursor |
none | Opaque cursor from a previous meta.nextCursor; a tampered value is a 422, never a silent reset to page one |
direction |
desc |
asc or desc |
sort |
per resource | See the resource’s filters |
{
"data": [],
"meta": { "nextCursor": "eyJ...", "total": 42 }
}meta.total is null when the filtered count is not obtainable cheaply; render that as “1–25 of many” rather than guessing.
Idempotency
POST capture, refund, void, cancel, transition, adjustment, and shipment-creation endpoints accept an Idempotency-Key header. A replay returns the first response byte-for-byte with Idempotent-Replay: true; the same key is also handed to the payment provider, so deduplication holds at both layers. Reusing a key with a different payload is 409 E_IDEMPOTENCY_CONFLICT.
Audit trail
Every mutation writes one append-only entry in the same database transaction as its effect, so the record and the change commit or roll back together. Entries carry the actor id and label, the ability, the subject, a scalar-only before/after summary, the idempotency key, and the request id. Summaries are PII-free by construction: the service rejects a summary carrying a name, address, email, phone, or credential-shaped field.
Read the trail through GET /api/commerce/admin/audit-entries, filterable by actorId, ability[], subjectType, subjectId, createdAfter, and createdBefore.
Endpoints
Catalog
| Method | Path | Route name | Ability |
|---|---|---|---|
GET |
/api/commerce/admin/products |
commerce.admin.products.index |
commerce.products.view |
POST |
/api/commerce/admin/products |
commerce.admin.products.store |
commerce.products.manage |
GET |
/api/commerce/admin/products/:id |
commerce.admin.products.show |
commerce.products.view |
PATCH |
/api/commerce/admin/products/:id |
commerce.admin.products.update |
commerce.products.manage |
DELETE |
/api/commerce/admin/products/:id |
commerce.admin.products.destroy |
commerce.products.manage |
POST |
/api/commerce/admin/products/:id/archive |
commerce.admin.products.archive |
commerce.products.manage |
POST |
/api/commerce/admin/products/:id/options |
commerce.admin.products.options.store |
commerce.products.manage |
POST |
/api/commerce/admin/products/:id/variants/generate |
commerce.admin.products.variants.generate |
commerce.products.manage |
PUT |
/api/commerce/admin/products/:id/categories |
commerce.admin.products.categories |
commerce.products.manage |
POST |
/api/commerce/admin/products/:id/images |
commerce.admin.products.images.store |
commerce.products.manage |
PATCH |
/api/commerce/admin/products/:id/images/:imageId |
commerce.admin.products.images.update |
commerce.products.manage |
PUT |
/api/commerce/admin/products/:id/images/order |
commerce.admin.products.images.order |
commerce.products.manage |
DELETE |
/api/commerce/admin/products/:id/images/:imageId |
commerce.admin.products.images.destroy |
commerce.products.manage |
GET |
/api/commerce/admin/categories |
commerce.admin.categories.index |
commerce.products.view |
POST |
/api/commerce/admin/categories |
commerce.admin.categories.store |
commerce.categories.manage |
PATCH |
/api/commerce/admin/categories/:id |
commerce.admin.categories.update |
commerce.categories.manage |
DELETE |
/api/commerce/admin/categories/:id |
commerce.admin.categories.destroy |
commerce.categories.manage |
Image upload uses multipart/form-data with one through four images file fields. Each file may be
JPEG, PNG, or WebP and at most 4 MiB; the service verifies the actual bytes. Update accepts
{ altText?: string | null, variantIds?: string[] }. Reorder accepts every current image exactly once
as { imageIds: string[] }; omissions, duplicates, and foreign image ids are rejected. Upload and
delete audit entries are committed with their database changes, and managed objects are cleaned up on
rollback or after committed deletion.
Orders
| Method | Path | Route name | Ability |
|---|---|---|---|
GET |
/api/commerce/admin/orders |
commerce.admin.orders.index |
commerce.orders.view |
GET |
/api/commerce/admin/orders/:id |
commerce.admin.orders.show |
commerce.orders.view |
POST |
/api/commerce/admin/orders/:id/transition |
commerce.admin.orders.transition |
commerce.orders.transition |
POST |
/api/commerce/admin/orders/:id/cancel |
commerce.admin.orders.cancel |
commerce.orders.cancel |
POST |
/api/commerce/admin/orders/:id/adjustments |
commerce.admin.orders.adjustments.store |
commerce.orders.adjust |
GET |
/api/commerce/admin/orders/:id/payments |
commerce.admin.orders.payments.index |
commerce.payments.view |
Payments
| Method | Path | Route name | Ability |
|---|---|---|---|
POST |
/api/commerce/admin/payments/:id/capture |
commerce.admin.payments.capture |
commerce.payments.capture |
POST |
/api/commerce/admin/payments/:id/refund |
commerce.admin.payments.refund |
commerce.payments.refund |
POST |
/api/commerce/admin/payment-intents/:id/void |
commerce.admin.paymentIntents.void |
commerce.payments.void |
Shipments
| Method | Path | Route name | Ability |
|---|---|---|---|
POST |
/api/commerce/admin/orders/:id/shipments |
commerce.admin.shipments.store |
commerce.shipments.manage |
POST |
/api/commerce/admin/shipments/:id/transition |
commerce.admin.shipments.transition |
commerce.shipments.manage |
Inventory
| Method | Path | Route name | Ability |
|---|---|---|---|
GET |
/api/commerce/admin/inventory/levels |
commerce.admin.inventory.index |
commerce.inventory.view |
POST |
/api/commerce/admin/inventory/adjustments |
commerce.admin.inventory.adjust |
commerce.inventory.adjust |
Customers
| Method | Path | Route name | Ability |
|---|---|---|---|
GET |
/api/commerce/admin/customers |
commerce.admin.customers.index |
commerce.customers.view |
GET |
/api/commerce/admin/customers/:id |
commerce.admin.customers.show |
commerce.customers.view |
POST |
/api/commerce/admin/customers/:id/anonymize |
commerce.admin.customers.anonymize |
commerce.customers.anonymize |
Channels and metrics
| Method | Path | Route name | Ability |
|---|---|---|---|
GET |
/api/commerce/admin/channels |
commerce.admin.channels.index |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/summary |
commerce.admin.metrics.summary |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/revenue |
commerce.admin.metrics.revenue |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/orders-by-status |
commerce.admin.metrics.ordersByStatus |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/top-products |
commerce.admin.metrics.topProducts |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/payment-mix |
commerce.admin.metrics.paymentMix |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/checkout-funnel |
commerce.admin.metrics.checkoutFunnel |
commerce.metrics.view |
GET |
/api/commerce/admin/metrics/low-stock |
commerce.admin.metrics.lowStock |
commerce.metrics.view |
Audit
| Method | Path | Route name | Ability |
|---|---|---|---|
GET |
/api/commerce/admin/audit-entries |
commerce.admin.audit.index |
commerce.audit.view |
Metrics
Every metrics endpoint takes currency (required), from, and to as ISO 8601 instants, plus an optional channelId. metrics/revenue also takes interval (day, week, or month); metrics/top-products and metrics/low-stock take limit.
Amounts are stringified minor units. Revenue is derived from the payment ledger — captures minus refunds — not from order totals, so the dashboard reconciles with the PPN and ledger exports to the cent. There is no cross-currency aggregation: one currency per request, mirroring the ledger’s integrity rule.
Customizing and upgrading
The generated files are yours. Add authentication strategies, response fields, observability, or extra guards directly in the application; keep the service-layer rule intact — controllers validate, authorize, and transform, and everything else belongs in a service so both admin surfaces and your jobs share it.
Run commerce:upgrade after upgrading the package. Files you changed are never overwritten; incoming versions are staged under .commercekit/incoming for review.