
REST API
MiniShop3 Web API (api.php) serves the storefront and headless clients: cart, checkout, customer account, public catalog. Manager API (connector.php) is separate, under a MODX session for the Vue admin.
Storefront route source: core/components/minishop3/config/routes/web.php. Custom: core/config/ms3_routes_web.custom.php, add-on fragments: core/config/ms3.routes.d/web/*.php.
Entry points
| Purpose | URL | Authorization |
|---|---|---|
| Web API (storefront / headless) | /assets/components/minishop3/api.php | Token: ms3_token cookie, Authorization: Bearer, legacy MS3TOKEN |
| Manager API (manager) | /assets/components/minishop3/connector.php | MODX session |
This page documents the Web API. Manager REST: Backend API, Routing.
Base URL
/assets/components/minishop3/api.php?route=/api/v1/{endpoint}All requests pass the route via the route parameter. For cookie tokens use credentials: 'include'. CORS and rate limit use ms3_cors_* and ms3_rate_limit_* (System settings).
Endpoint map
Source: config/routes/web.php. The /api/v1 group always runs CORS, rate limit, and ServiceCheck.
| Method | Path | Token |
|---|---|---|
POST | /cart/add | guest |
POST | /cart/remove | guest |
POST | /cart/change | guest |
POST | /cart/change-option | guest |
GET | /cart/get | guest |
POST | /cart/clean | guest |
GET | /order/get | guest |
POST | /order/add | guest |
POST | /order/set | guest |
POST | /order/remove | guest |
POST | /order/submit | guest |
POST | /order/clean | guest |
GET | /order/cost | guest |
GET | /order/cost/cart | guest |
GET | /order/cost/delivery | guest |
GET | /order/cost/payment | guest |
POST | /order/address/set | guest |
POST | /order/address/clean | guest |
GET | /order/delivery/validation-rules | guest |
GET | /order/delivery/required-fields | guest |
POST | /customer/login | none |
POST | /customer/register | none |
POST | /customer/logout | authorized |
POST | /customer/forgot-password | none |
POST | /customer/reset-password | none |
POST | /customer/add | authorized |
GET | /customer/token/get | none |
GET | /customer/addresses | authorized |
GET | /customer/addresses/{id} | authorized |
POST | /customer/addresses | authorized |
PUT | /customer/addresses/{id} | authorized |
DELETE | /customer/addresses/{id} | authorized |
PUT | /customer/addresses/{id}/set-default | authorized |
PUT | /customer/profile | authorized |
POST | /customer/changeAddress | guest |
POST | /customer/email/resend-verification | authorized |
GET | /customer/email/verify | none |
GET | /customer/orders | authorized |
GET | /customer/orders/{id} | authorized |
POST | /customer/orders/{id}/cancel | authorized |
GET | /product/get/{id} | none |
GET | /product/list | none |
GET | /health | none |
“Guest” token: GET /customer/token/get (cart and order draft). “Authorized”: after login / register.
Programmatic order creation without an HTTP session (extras/cron) is not Web HTTP. See ProgrammaticOrderService.
Authorization
Getting a token
Before working with the cart and orders you need to get a client token:
GET /api/v1/customer/token/getResponse:
{
"success": true,
"data": {
"token": "abc123def456..."
},
"message": ""
}Token storage (httpOnly cookie)
As of v1.6, the token is stored in an httpOnly cookie ms3_token. The server sets the cookie when the token is obtained or refreshed.
Security
The httpOnly cookie is not accessible from JavaScript, which protects the token from XSS. The browser sends the cookie with every request.
Token resolution order on the server (TokenMiddleware):
Authorization: Bearer {token}header (for mobile apps)HTTP_MS3TOKENheader (legacy)- httpOnly cookie
ms3_token(primary for web)
The cookie is configured using MODX session parameters: session_cookie_domain, session_cookie_path, session_cookie_secure, session_cookie_samesite.
Response format
All responses use a single format:
Success:
{
"success": true,
"data": { ... },
"message": "Success message"
}Error:
{
"success": false,
"message": "Error description",
"code": 400
}Cart
Add product
POST /api/v1/cart/addParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
id | int | Yes | Product ID |
count | int | No | Quantity (default 1) |
options | object | No | Product options (color, size, etc.) |
render | array | No | Snippet tokens for SSR |
Request example:
fetch('/assets/components/minishop3/api.php?route=/api/v1/cart/add&ms3_token=' + token, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 123,
count: 2,
options: {
color: 'Red',
size: 'XL'
}
})
})Response:
{
"success": true,
"data": {
"last_key": "123_a1b2c3d4",
"cart": [
{
"key": "123_a1b2c3d4",
"id": 123,
"count": 2,
"price": 1500,
"cost": 3000,
"weight": 0.5,
"options": {"color": "Red", "size": "XL"},
"name": "Product",
"thumb": "/assets/images/product.jpg"
}
],
"status": {
"total_count": 2,
"total_cost": 3000,
"total_weight": 1.0,
"total_positions": 1
}
},
"message": "Product added to cart"
}Change quantity
POST /api/v1/cart/changeParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
product_key | string | Yes | Unique product key in cart |
count | int | Yes | New quantity |
Example:
{
"product_key": "123_a1b2c3d4",
"count": 5
}Remove product
POST /api/v1/cart/removeParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
product_key | string | Yes | Unique product key |
Change product options
POST /api/v1/cart/change-optionUpdates options of a cart line (the line key may change).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
product_key | string | Yes | Cart line key |
options | object | Yes | New options (non-empty object) |
Example:
{
"product_key": "123_a1b2c3d4",
"options": {
"color": "red",
"size": "L"
}
}Get cart
GET /api/v1/cart/getResponse:
{
"success": true,
"data": {
"cart": [...],
"status": {
"total_count": 5,
"total_cost": 7500,
"total_weight": 2.5,
"total_positions": 3
}
}
}Clear cart
POST /api/v1/cart/cleanOrder
Get order draft
GET /api/v1/order/getResponse:
{
"success": true,
"data": {
"order": {
"id": 0,
"delivery_id": 1,
"payment_id": 1,
"order_comment": "",
"cart_cost": 1500,
"delivery_cost": 300,
"cost": 1800,
"address_email": "user@example.com",
"address_phone": "+79991234567",
"address_first_name": "John",
"address_last_name": "Doe",
"address_city": "Moscow",
"address_street": "Main St",
"address_comment": ""
}
}
}data contains only the order object (msOrder fields + address with address_ prefix). This endpoint does not return delivery or payment method lists.
Add/update field
POST /api/v1/order/addParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Field name |
value | mixed | Yes | Value |
Available fields:
| Field | Description |
|---|---|
email | Email (written to address) |
phone | Phone (address) |
first_name | First name (address) |
last_name | Last name (address) |
delivery_id | Delivery method ID |
payment_id | Payment method ID |
order_comment | Order comment (msOrder) |
comment | Address comment (msOrderAddress) |
city | City |
street | Street |
building | Building |
room | Apartment/office |
index | Postal code |
address_hash | Saved address hash |
In add / set, address keys have no prefix (city, first_name). In the order/get response the same fields arrive as address_city, address_first_name.
Example:
{
"key": "email",
"value": "user@example.com"
}Set multiple fields
POST /api/v1/order/setParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
fields | object | Yes | Object with fields |
Example:
{
"fields": {
"email": "user@example.com",
"phone": "+79991234567",
"first_name": "John",
"delivery_id": 1,
"payment_id": 2,
"order_comment": "Call before delivery"
}
}If any field fails, the response is success: false, message ms3_order_err_validation, and data:
{
"order": { },
"errors": {
"email": "…",
"delivery_id": "…"
}
}Each field still goes through add() and events msOnBeforeAddToOrder / msOnAddToOrder. set() only aggregates errors.
Remove field
POST /api/v1/order/removeParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Field name |
Submit order
POST /api/v1/order/submitResponse (success):
{
"success": true,
"data": {
"order_id": 15,
"order_num": "24/12-15",
"redirect_url": "/thank-you?msorder=15"
},
"message": "Order submitted successfully"
}Response (validation error):
{
"success": false,
"message": "Fill required fields",
"data": {
"errors": {
"email": "Enter email",
"phone": "Enter phone"
}
},
"code": 400
}Clear order
POST /api/v1/order/cleanCost
Total cost
GET /api/v1/order/costResponse:
{
"success": true,
"data": {
"cart_cost": 5000,
"delivery_cost": 300,
"payment_cost": 0,
"total_cost": 5300,
"discount": 0
}
}Cart cost
GET /api/v1/order/cost/cartDelivery cost
GET /api/v1/order/cost/deliveryPayment fee
GET /api/v1/order/cost/paymentDelivery addresses
Set saved address
POST /api/v1/order/address/setParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
address_hash | string | Yes | Address MD5 hash |
Clear address
POST /api/v1/order/address/cleanDelivery validation
Validation rules
GET /api/v1/order/delivery/validation-rulesResponse:
{
"success": true,
"data": {
"city": {"required": true, "min": 2},
"street": {"required": true},
"building": {"required": true},
"phone": {"required": true, "pattern": "^\\+?[0-9]+$"}
}
}Required fields
GET /api/v1/order/delivery/required-fieldsResponse:
{
"success": true,
"data": ["city", "street", "building", "phone"]
}Customer
Registration
POST /api/v1/customer/registerParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | |
password | string | Yes | Password |
first_name | string | No | First name |
last_name | string | No | Last name |
phone | string | No | Phone |
privacy_accepted | bool | Depends on settings | Data processing consent |
The controller passes only these fields to the processor. password_confirm is not used for Web API registration (it is required for reset-password).
Response:
{
"success": true,
"object": {
"customer": {
"id": 5,
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": "+79991234567",
"email_verified": false
},
"token": "abc123def456...",
"expires_at": "2026-03-16 12:34:56",
"email_verification_required": false,
"redirect_url": ""
},
"message": "Registration successful"
}Breaking change (v1.6)
Registration response format changed:
- Before (v1.5):
token— object{token: "...", expires_at: "..."} - After (v1.6):
token— string,expires_atat top level
Custom themes that use result.object.token.token should switch to result.object.token.
Login
POST /api/v1/customer/loginParameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | |
password | string | Yes | Password |
Response:
{
"success": true,
"data": {
"customer_id": 5,
"token": "session_token_xyz789",
"expires_at": "2026-08-19 12:00:00",
"customer": {
"id": 5,
"email": "user@example.com",
"first_name": "John"
}
}
}Token rotation
After login / register / successful email/verify, the server always issues a new API token (AuthManager::establishCustomerSession). The old guest or previous ms3_token cookie is revoked. The cart draft moves to the new token (transferDraftToToken / bindDraftToCustomer), then session_regenerate_id(true).
A headless client must persist the new token / expires_at from the response. On a storefront with an httpOnly cookie, the browser updates the cookie itself.
const res = await fetch('/api/v1/customer/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'ms3-token': guestToken, // current guest token
},
body: JSON.stringify({ email, password }),
})
const json = await res.json()
if (!json.success) throw new Error(json.message)
// Replace the token for all subsequent requests
const { token, expires_at, customer_id } = json.data
localStorage.setItem('ms3_token', token)
localStorage.setItem('ms3_token_expires', expires_at)Logout
POST /api/v1/customer/logoutRequires an authorized token. Ends the customer session.
Forgot password
POST /api/v1/customer/forgot-passwordNo token required. Rate limit per email (1 request / 5 minutes).
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Customer email |
The response stays success-shaped for UX (does not reveal whether the account exists). Email is sent only if the customer exists.
Reset password
POST /api/v1/customer/reset-passwordNo auth token required (reset token comes from the email).
| Parameter | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Token from email |
password | string | Yes | New password |
password_confirm | string | Yes | Password confirmation |
Update profile
PUT /api/v1/customer/profileRequires authorization (authenticated customer token).
Parameters:
| Parameter | Type | Description |
|---|---|---|
first_name | string | First name |
last_name | string | Last name |
phone | string | Phone |
Quick profile field update
POST /api/v1/customer/addRequires authorization. Updates one msCustomer field (key + value). Allowed editable keys come from the xPDO map (with a denylist of system fields). Changing email resets email_verified_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Field name |
value | mixed | Yes | New value |
Select saved address in draft
POST /api/v1/customer/changeAddressRequires a guest token. Same as POST /order/address/set: applies an address by address_hash (legacy value is also accepted).
| Parameter | Type | Required | Description |
|---|---|---|---|
address_hash | string | Yes | Customer address hash |
Email verification
GET /api/v1/customer/email/verify?token=verification_tokenResend verification
POST /api/v1/customer/email/resend-verificationRequires authorization.
Customer addresses
All endpoints require authorization.
List addresses
GET /api/v1/customer/addressesResponse:
{
"success": true,
"data": [
{
"id": 1,
"hash": "abc123...",
"city": "Moscow",
"street": "Main St",
"building": "10",
"room": "5",
"is_default": true
}
]
}Get address
GET /api/v1/customer/addresses/{id}Create address
POST /api/v1/customer/addressesParameters:
| Parameter | Type | Description |
|---|---|---|
city | string | City |
street | string | Street |
building | string | Building |
room | string | Apartment/office |
index | string | Postal code |
country | string | Country |
region | string | Region |
is_default | bool | Default address |
Update address
PUT /api/v1/customer/addresses/{id}Delete address
DELETE /api/v1/customer/addresses/{id}Set default address
PUT /api/v1/customer/addresses/{id}/set-defaultCustomer orders
Requires an authorized customer token. Drafts are not returned in the list or detail.
Order list
GET /api/v1/customer/orders?limit=20&offset=0&status=2| Parameter | Type | Description |
|---|---|---|
limit | int | Page size (default 20, max 100) |
offset | int | Offset |
status | int | Filter by status_id. Draft and invalid IDs are ignored |
Response data:
{
"orders": [
{
"id": 15,
"uuid": "...",
"num": "2603/1",
"cost": 3500,
"status_id": 2,
"status_name": "New",
"can_cancel": true
}
],
"total": 1,
"limit": 20,
"offset": 0
}Order detail
GET /api/v1/customer/orders/{id}Returns the customer order with products and related entities. 404 if the order belongs to someone else, is missing, or is a draft.
Cancel order
POST /api/v1/customer/orders/{id}/cancelCancels the order if the current status is in ms3_customer_cancel_allowed_statuses.
Response (success):
{
"success": true,
"message": "Order canceled",
"data": {
"order_id": 15,
"status_id": 5
}
}Errors: 400 (status not allowed), 404 (not found), 401 (unauthorized).
Related settings:
| Setting | Description |
|---|---|
ms3_customer_cancel_allowed_statuses | Status IDs for which cancellation is allowed (default 2,3) |
ms3_status_canceled | Target status ID for canceled orders |
Product catalog
Public endpoints. No token required. Only published, non-deleted, non-hidemenu products in the requested (or current) context are returned.
ProductCatalogService builds the response and trims it with an allowlist of resource and msProductData fields. A plugin on msOnGetProductFields can change values of existing keys but cannot add arbitrary fields to the catalog JSON. For a headless storefront without msProducts see also Catalog.
Single product
GET /api/v1/product/get/{id}| Path parameter | Description |
|---|---|
id | Product resource ID |
400 without id, 404 if the product is missing or not public.
Product list
GET /api/v1/product/list?parent=5&limit=20&page=1&sort=price&dir=ASC| Parameter | Type | Description |
|---|---|---|
parent / category | int | Parent category ID (resource primary parent) |
limit | int | Default 20, max 100 |
offset | int | Offset. Alternative: page (from 1) |
page | int | Page number when offset is omitted |
sort | string | id, pagetitle, menuindex, createdon, publishedon, price, article |
dir / sortdir | string | ASC or DESC |
query | string | Search by pagetitle / article |
context | string | MODX context key |
include_options | 0 / 1 | Include options (default 0) |
include_content | 0 / 1 | Include content (default 0) |
Response data:
{
"items": [ { "id": 10, "pagetitle": "Product", "price": 1500 } ],
"total": 42,
"limit": 20,
"offset": 0
}Health Check
GET /api/v1/healthResponse:
{
"success": true,
"data": {
"status": "ok",
"version": "1.0.0",
"timestamp": 1703952000,
"api": "web"
}
}Middleware
CORS
Configured via system setting ms3_cors_allowed_origins:
*— allow all domainshttps://example.com,https://shop.example.com— list of domains
Rate Limiting
Abuse protection via system settings:
ms3_rate_limit_max_attempts— max requests (default 60)ms3_rate_limit_decay_seconds— period in seconds (default 60)ms3_rate_limit_store— counter store:file,redis,memcached(defaultfile)ms3_rate_limit_storage_path— directory forfile(empty = system temp)ms3_rate_limit_redis_dsn— Redis DSN (overrides host/port when set)ms3_rate_limit_redis_host/ms3_rate_limit_redis_port/ms3_rate_limit_redis_password/ms3_rate_limit_redis_databasems3_rate_limit_memcached_servers— Memcached servers (default127.0.0.1:11211)
When limit is exceeded:
{
"success": false,
"message": "Too many requests",
"code": 429
}SSR (Server-Side Rendering)
The API supports server-side HTML rendering for updating parts of the page.
Usage
Pass an array of snippet tokens in the render parameter:
fetch('/api/v1/cart/add?ms3_token=' + token, {
method: 'POST',
body: JSON.stringify({
id: 123,
render: ['ms3_abc123...', 'ms3_def456...']
})
})Response includes HTML:
{
"success": true,
"data": {
"cart": [...],
"status": {...},
"render": {
"ms3_abc123...": "<div class=\"cart\">...</div>",
"ms3_def456...": "<span class=\"count\">5</span>"
}
}
}Registering snippets
Tokens are generated automatically when calling snippets with the selector parameter:
{'msCart' | snippet: [
'tpl' => 'tpl.msCart',
'selector' => '#cart-container'
]}Custom routes
To add your own endpoints create a file:
core/config/ms3_routes_web.custom.phpExample:
<?php
use MiniShop3\Router\Response;
$router->group('/api/v1', function($router) use ($modx) {
$router->get('/custom/endpoint', function($params) use ($modx) {
return Response::success(['custom' => 'data']);
});
});Custom routes are loaded after system routes and can override them.
JavaScript client
MiniShop3 provides a JavaScript library for working with the API:
// Add to cart
await ms3.cartAPI.add(123, 2, { color: 'red' })
// Submit order
const result = await ms3.orderAPI.submit()
// Hooks
ms3Hooks.addHook('afterAddCart', async ({ response }) => {
console.log('Product added', response.data)
})See Frontend JavaScript for details.
Error codes
| Code | Description |
|---|---|
| 400 | Bad request (missing parameters, validation error) |
| 401 | Authorization token required |
| 403 | Access denied |
| 404 | Resource not found |
| 429 | Too many requests |
| 500 | Internal server error |
Debugging
Enable debug mode via the ms3_api_debug setting:
{
"success": false,
"message": "Internal server error",
"code": 500,
"debug": {
"exception": "Exception",
"message": "Detailed error message",
"file": "/path/to/file.php",
"line": 123
}
}Security
Do not enable debug mode in production — it exposes the application's internal structure.
