🌐 Language: 🇻🇳 Tiếng Việt · 🇬🇧 English (current) · 🇯🇵 日本語
Admin API (api/v2/core)
Introduction
This page shows how to work with the admin API: sign in with an administrator account, read orders, products and customers as on the admin screens, and write data (order status, shipping, prices, stock, locking customers). It is for people connecting an ERP, warehouse software, CRM or internal tool to the site. Every call follows the administrator account's own permissions and store (see Concepts you need); details of each endpoint are in Reference — Admin.
Core API workflow (Admin)
The diagram below shows a management app signing in and then reading data through the Core API. Every request goes through the checks in the order shown; the first check that refuses answers with an error straight away.
sequenceDiagram
autonumber
participant A as Management app
participant M as GP247 API<br/>(shared checks)
participant S as Sanctum<br/>(tokens)
participant Q as Permissions<br/>(admin screen + store)
participant D as Database
Note over A,M: Every request, login included: API switch (group turned off: 404 api_disabled) → force JSON<br/>→ API Connection (if enabled: apiconnection + apikey headers, wrong = 401) → rate limit
A->>M: POST /api/v2/core/login {username, password, remember_me?, with_refresh?}
alt Login limit reached (5/min per account, 20/min per IP)
M-->>A: 429 too_many_attempts + Retry-After
else Wrong password or admin account disabled
M-->>A: 401 unauthenticated
else The account needs an extra verification step (Two-factor authentication (MFA) through a plugin)
M-->>A: 401 challenge_required + challenge
else Valid
M->>S: Create token (scope admin / admin-supper)
S-->>M: access_token
M-->>A: 200 {data: {access_token, token_type, scopes, expires_at, …}}
end
A->>M: GET /api/v2/core/order/list<br/>Authorization: Bearer access_token
M->>S: Check token: not expired, admin token type
alt Missing, wrong or expired token, or admin disabled
M-->>A: 401 unauthenticated
else Valid token
M->>Q: Allowed to open the "Order list" screen? Right store domain?
alt No permission or wrong store domain
M-->>A: 403 forbidden
else Allowed
Q-->>M: Store of the account
M->>D: Query only inside that store
D-->>M: Data
M-->>A: 200 {data, meta, links} — a record you cannot see answers 404 not_found
end
end
A->>M: POST /api/v2/core/logout
M-->>A: 200 — session signed out
Takeaway: a token is not enough to read data — the account also needs permission to open the matching admin screen, and it only sees the data of its own store (Permissions and data scope of the Core API).
Login & session endpoints (prefix api/v2/core):
| Method | Path | Login required | Function |
|---|---|---|---|
POST |
api/v2/core/login |
No | Admin login, returns access_token (optional with_refresh — Refresh tokens — stay signed in with short-lived tokens) |
POST |
api/v2/core/login/challenge |
No | Finish a sign-in that needs an extra verification step (Two-factor authentication (MFA) through a plugin) |
POST |
api/v2/core/refresh |
No (send the refresh token) | Exchange a refresh token for a new token pair (Refresh tokens — stay signed in with short-lived tokens) |
POST |
api/v2/core/logout |
Yes (Bearer) | Sign the current session out (refresh token included) |
GET |
api/v2/core/info |
Yes (Bearer) | Get the currently logged-in administrator's info (with roles) |
GET |
api/v2/core/sessions |
Yes (Bearer) | The admin's API sign-in sessions (devices) (Managing sign-in sessions) |
DELETE |
api/v2/core/sessions/{id} |
Yes (Bearer) | Sign one session out |
Admin data endpoints (prefix api/v2/core, token + permission from Permissions and data scope of the Core API required):
| Method | Path | Function |
|---|---|---|
GET |
api/v2/core/customer/list |
Customer list |
GET |
api/v2/core/customer/detail/{id} |
A customer's details |
GET |
api/v2/core/order/list |
Order list |
GET |
api/v2/core/order/detail/{id} |
An order's details |
GET |
api/v2/core/product/list |
Product list (with stock and cost price) |
GET |
api/v2/core/product/detail/{id} |
A product's details |
GET |
api/v2/core/category/list |
Category list |
GET |
api/v2/core/category/detail/{id} |
A category's details |
GET |
api/v2/core/brand/list |
Brand list |
GET |
api/v2/core/brand/detail/{id} |
A brand's details |
GET |
api/v2/core/supplier/list |
Supplier list |
GET |
api/v2/core/supplier/detail/{id} |
A supplier's details |
GET |
api/v2/core/banner/list |
Banner list |
GET |
api/v2/core/banner/detail/{id} |
A banner's details |
GET |
api/v2/core/page/list |
Content page list |
GET |
api/v2/core/page/detail/{id} |
A content page's details |
GET |
api/v2/core/events |
Event log for ERP / sync tools (Response format, list parameters & error codes) |
POST |
api/v2/core/order/{id}/status |
Change an order's status (Writing data through the admin API) |
POST |
api/v2/core/order/{id}/shipping |
Change the shipping status, keep the tracking number |
POST |
api/v2/core/order/{id}/notes |
Add a note to the order history |
PATCH |
api/v2/core/product/{id} |
Change a product's selling price, cost price, status (Writing data through the admin API) |
POST |
api/v2/core/product/{id}/stock |
Receive / issue goods, set the stock after a stock count |
POST |
api/v2/core/stock/batch |
Write the stock of many products at once (up to 100 lines) |
PATCH |
api/v2/core/customer/{id} |
Lock / unlock a customer (Customers) |
Example admin login (using curl in a Terminal):
curl -X POST "https://shop.example.com/api/v2/core/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password","remember_me":true}'
On success, you receive JSON like (shortened):
{
"data": {
"access_token": "1|aBcD...very-long...XyZ",
"token_type": "Bearer",
"scopes": ["admin-supper"],
"expires_at": "2026-11-25 10:00:00",
"expires_in": 2592000
}
}
Then fetch the latest orders using the token you just received (data.access_token):
curl -X GET "https://shop.example.com/api/v2/core/order/list?sort=-created_at&page[size]=20" \
-H "Accept: application/json" \
-H "Authorization: Bearer 1|aBcD...very-long...XyZ"
Writing data through the admin API
ℹ️ Available since: gp247/core 3.7.0, gp247/shop 3.6.0
An ERP, CRM or warehouse tool can update orders, product prices and stock, and lock / unlock customers through the API instead of doing it by hand on the admin area. Each order write has the same effect as the matching action on the order details screen: same stock movements, same order history lines.
| Method | Path | Request body (JSON) | Effect |
|---|---|---|---|
POST |
api/v2/core/order/{id}/status |
{"status": …} |
Change the order's status |
POST |
api/v2/core/order/{id}/shipping |
{"shipping_status"?, "tracking_number"?, "tracking_url"?} |
Change the shipping status, keep the tracking number |
POST |
api/v2/core/order/{id}/notes |
{"content": "…"} |
Add a note to the order history |
PATCH |
api/v2/core/product/{id} |
{"price"?, "cost"?, "status"?} |
Change the product's selling price, cost price, status |
POST |
api/v2/core/product/{id}/stock |
{"delta": …} or {"set": …} |
Receive / issue goods, or set the stock after a stock count |
POST |
api/v2/core/stock/batch |
{"items": [{"id" or "sku", "delta" or "set"}, …]} |
Write the stock of many products (1–100 lines) |
PATCH |
api/v2/core/customer/{id} |
{"status": 0} or {"status": 1} |
Lock / unlock a customer |
Common rules (the three order endpoints):
- Permission: the account needs change permission on the Order list screen, checked exactly as when using an
action on that screen — whoever may change orders on the screen may change them through the API. View-only accounts
(including the
view.allrole) →403 forbidden. - Only orders of your own store (Permissions and data scope of the Core API); an order of another store →
404 not_found. - Success →
200 {"data": <order>}, same shape asorder/detail/{id}, already including the change. - Invalid data →
422 validation_failedwitherrors. - Every write shows in the Operation log like any admin API call (Admin API access log).
Order status — order/{id}/status:
statusis the id of one of the site's order statuses; an unknown id →422.- Same effect as changing the status on the admin screen: cancelling returns the goods to stock, reopening a cancelled order takes them out of stock again, and every change adds an order history line. A finished or cancelled order can be reopened, as on the screen.
- The order already has the status sent →
200, nothing written. - Reopening refused (for example not enough stock) →
409, with the reason incode, for exampleadmin.order.reopen_blocked_stock.
Shipping — order/{id}/shipping:
- Send at least
shipping_statusortracking_number.tracking_urlneedstracking_numberand must be anhttp(s)address. shipping_status: the id of one of the site's shipping statuses; a change adds an order history line.tracking_number: kept as the order's manual shipment (one per order; sending another number updates it), shown on the order details screen.- Sending
payment_status→422: the payment status always follows the order's payments and cannot be set by hand.
Notes — order/{id}/notes:
contentis 1–1000 characters; the note goes into the order history with its author and the label[API].- The text is shown as plain text: HTML tags are shown as written, never run.
Example: the ERP reports that an order was handed to the carrier, with its tracking number:
curl -X POST "https://shop.example.com/api/v2/core/order/ORDER-ID/shipping" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 1|aBcD...very-long...XyZ" \
-H "Idempotency-Key: 6f1c2a7e-0b9d-4c51-9e3a-2d8f4b7c1a90" \
-d '{"shipping_status":2,"tracking_number":"VN123456789","tracking_url":"https://tracking.example.com/VN123456789"}'
Result (shortened):
{
"data": {
"id": "ORDER-ID",
"status": 2,
"payment_status": 3,
"shipping_status": 2,
"total": 350000,
"balance": 0,
"updated_at": "2026-10-31T09:15:00+07:00"
}
}
Products and stock
Common rules (the three product and stock endpoints):
- Permission: the account needs change permission on the Product list screen, checked exactly as the order
writes are checked against the Order list screen. View-only accounts →
403 forbidden. - Only products of your own store; a product of another store →
404 not_found(instock/batch: anot_foundline). - Invalid data →
422 validation_failedwitherrors.
Price and status — PATCH product/{id}:
- Send at least one of
price,cost(a number ≥ 0),status(0or1). An empty body →422. - Any other field (for example
sku,stock) →422 validation_failed; it is not silently ignored. Stock is written with the stock commands below. - A field turned off in the store's Config product settings (Use PRICE / Use COST PRICE) →
422: the API follows the same field switches as the product form on the admin area, for the product's own store. - Success →
200 {"data": <product>}, same shape asproduct/detail/{id}. Idempotency-Keyis optional, as for the order writes.
Stock of one product — product/{id}/stock: the body carries exactly one of two fields.
| Field | Meaning | Rules |
|---|---|---|
delta |
Receive goods (positive) or issue goods (negative) | Not 0; whole numbers unless Allow decimal quantity is on in the product settings. The stock never goes below 0 → 422 stock_would_be_negative, nothing changes |
set |
Set the stock outright after a stock count | ≥ 0; only an account with the administrator role (others → 403) |
- Both fields or neither →
422 validation_failed. deltaadds or subtracts atomically: an order placed at the same moment loses nothing. The product's sold counter does not change — this is not a sale.- A build product (a combo whose stock comes from its components) →
422 stock_managed_by_components. - The
Idempotency-Keyheader is required (missing →400 idempotency_key_required): a retry with the same key moves the stock only once and answers the first result with the headerIdempotent-Replayed: true. - Success →
200 {"data": {"id": "…", "sku": "…", "stock": 15}}.
💡 Why send
deltaand not the new stock figure? Between the moment the ERP reads the stock and the moment it sends a new figure, a customer may place an order. Sending the new figure would overwrite what that order took; sending adelta(receive +20, issue −3) adds or subtracts on the current figure. Usesetonly right after a physical stock count.
Example: the warehouse tool reports 20 units received:
curl -X POST "https://shop.example.com/api/v2/core/product/PRODUCT-ID/stock" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 1|aBcD...very-long...XyZ" \
-H "Idempotency-Key: 3b9e7d10-5c2f-4a8e-b1d4-7f0a6c2e9b35" \
-d '{"delta":20}'
Result:
{ "data": { "id": "PRODUCT-ID", "sku": "TSHIRT-01", "stock": 35 } }
Many products at once — stock/batch:
itemshas 1–100 lines; each line has the product'sidorsku, withdeltaorset(same rules as above). More than 100 lines or a malformed line (for example neitherdeltanorset) →422, no line runs.- Each line has its own result; a bad line does not stop the others. HTTP stays
200even when some lines fail — readok/failedand each result. resultsare in the same order asitems; each result echoes the line'sidorsku.Idempotency-Keyis required: a retried batch moves nothing twice and answers the same body.
Line code |
When |
|---|---|
not_found |
The id is not in the account's store |
sku_not_found |
No product has this SKU |
sku_ambiguous |
Several products share the SKU — send the id |
forbidden |
set by an account that is not an administrator |
stock_would_be_negative |
The issue would take the stock below 0 — this line does not run |
stock_managed_by_components |
A build product; its stock comes from its components |
Example:
curl -X POST "https://shop.example.com/api/v2/core/stock/batch" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 1|aBcD...very-long...XyZ" \
-H "Idempotency-Key: 9a4d2c61-7e3b-4f05-8c1a-0b6e5d3f2a78" \
-d '{"items":[{"id":"PRODUCT-ID","delta":20},{"sku":"MUG-07","delta":-3}]}'
Result:
{
"data": {
"results": [
{ "id": "PRODUCT-ID", "ok": true, "stock": 35 },
{ "sku": "MUG-07", "ok": false, "code": "sku_not_found", "msg": "No product has this SKU" }
],
"ok": 1,
"failed": 1
}
}
Customers
A CRM or ERP locks a customer account (for example after fraud or overdue debt) and unlocks it again through
PATCH customer/{id}.
- The body holds only
status(required):0locks,1unlocks. A value other than0/1→422. - Any other field (for example
email,first_name) →422 validation_failed: personal data is changed by the customer or on the admin area, never by a sync tool. - Permission: change permission on the Customer list screen, checked exactly as on that screen. View-only
accounts →
403 forbidden. - Only customers of your own store; a customer of another store →
404 not_found. - A lock bites at once: the customer's API tokens are refused from the next request (
401), and a customer signed in on the storefront is signed out. - Success →
200 {"data": <customer>}, same shape ascustomer/detail/{id}. Idempotency-Keyis optional, as for the order writes.
Example of a CRM locking a customer:
curl -X PATCH "https://shop.example.com/api/v2/core/customer/CUSTOMER-ID" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 1|aBcD...very-long...XyZ" \
-H "Idempotency-Key: 5d8e1f02-a6b4-4c37-9e2d-1b7c0a4f6e93" \
-d '{"status":0}'
Result (trimmed):
{
"data": {
"id": "CUSTOMER-ID",
"email": "customer@example.com",
"status": 0,
"updated_at": "2026-11-01T10:20:00+07:00"
}
}
Retrying safely with Idempotency-Key
On a flaky network the app cannot tell whether a write ran. Send an Idempotency-Key header (8–255 printable characters,
for example a UUID) to retry without writing twice. On the three order endpoints, PATCH product/{id} and
PATCH customer/{id} the key is optional; on product/{id}/stock and stock/batch it is required (missing key → 400 idempotency_key_required).
sequenceDiagram
participant E as ERP
participant API
E->>API: POST order/ORDER-ID/status + Idempotency-Key K
API-->>E: 200 (network drops, the ERP never gets it)
E->>API: same key K, same body again
API-->>E: 200 first result + Idempotent-Replayed: true
| Situation | Result |
|---|---|
| Same key, same body again | The first result is answered again, with the header Idempotent-Replayed: true; nothing runs again |
| Same key, another request (other body or path) | 409 idempotency_key_reused |
| Same key, first request still being processed | 409 idempotency_in_progress — wait, then send again |
First request hit a server error (5xx) |
The key is freed: a retry really runs |
| Malformed key, or missing on a stock write | 400 idempotency_key_required |
Site has not run php artisan gp247:update |
503 idempotency_unavailable (only when a key is sent) |
- Keys are separate for each admin account and kept 24 hours. Each new write needs a new key.
4xxresults (for example409,422) are stored too: retrying with the same key gets the same error — after fixing the data, use a new key.
Write rate limit
- Writes are counted per admin account: by default 300 requests / minute. Change it with the environment variable
GP247_API_WRITE_THROTTLEasrequests,minutes, for example600,1. - Product, stock, order and customer writes share this limit; one
stock/batchcounts as one request. - Over the limit →
429 too_many_attemptswith aRetry-Afterheader. - Reads keep the general per-IP limit.
Q&A
Q1: Which administrator account should an ERP use?
→ Create a separate account for the ERP and grant only the screens it needs (for example Orders, Products). Do not use a real person's account or the top administrator account.
Q2: If the ERP resends a write call after a network drop, is stock taken twice?
→ No, as long as it sends the same Idempotency-Key header as the first time: the site returns the earlier result instead of doing it again (see Retrying safely above).
📅 Last updated: 2026-11-03 · ✍️ Author: GP247