Introduction
The storeVero API lets you build themes, apps and integrations on top of every storeVero store. It is organised around REST: predictable resource URLs, form- or JSON-encoded request bodies, JSON responses, and standard HTTP verbs and status codes. If you have called Stripe or Shopify, this will feel familiar.
Base URL & versioning
All Admin API requests are made to the versioned base URL below. The Storefront API is public and scoped to a single store by its handle.
Admin API https://storevero.com/api/v1
Storefront API https://storevero.com/api/v1/ecommerce/storefront/{store-handle}
GraphQL https://storevero.com/api/v1/storefront/graphql
The current version is v1. Breaking changes ship under a new version prefix; additive changes (new fields, new endpoints) can arrive within v1, so write tolerant parsers that ignore unknown fields.
Authentication
The Admin API authenticates with a Bearer token. Exchange your storeVero email + password at the auth endpoint below — there is no API-keys screen in the dashboard yet. Send the token in the Authorization header on every request. A token acts as that user in every store they belong to, has no scopes and does not expire, so keep it on your server and never in client-side code.
To pick the store, send X-Workspace-Id: <store id> (the workspaces list in the login response has the ids). Without it, the user's current store is used.
Get a token
/auth/logincurl -X POST https://storevero.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@store.com","password":"••••••••"}'
const res = await fetch('https://storevero.com/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({ email: 'you@store.com', password: '••••••••' })
});
const { data } = await res.json(); // { user, token, workspaces }
const token = data.token;
$res = Http::acceptJson()->post('https://storevero.com/api/v1/auth/login', [
'email' => 'you@store.com',
'password' => '••••••••',
]);
$token = $res->json('data.token');
Authenticate a request
curl https://storevero.com/api/v1/ecommerce/products \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "X-Workspace-Id: 12" \ -H "Accept: application/json"
const res = await fetch('https://storevero.com/api/v1/ecommerce/products', {
headers: {
'Authorization': `Bearer ${token}`,
'X-Workspace-Id': '12',
'Accept': 'application/json'
}
});
$res = Http::withToken($token)
->withHeaders(['X-Workspace-Id' => 12])
->acceptJson()
->get('https://storevero.com/api/v1/ecommerce/products');
POST /shopify/storefront-tokens {"title": "My app"} (admin token required) and pass its access_token in the X-Shopify-Storefront-Access-Token header. The storefront REST routes under your store handle need no token at all.
Rate limits
Every API route shares one limiter with a fixed one-minute window. It is the same on every plan. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining, so you can slow down before you are throttled.
| Caller | Requests / minute | Counted per |
|---|---|---|
| With a Bearer token | 300 | user |
| Without a token (storefront routes) | 120 | IP address |
Over the limit you receive 429 Too Many Requests with a Retry-After header (seconds until the window resets).
Errors
storeVero uses conventional HTTP status codes. 2xx means success, 4xx means the request was rejected (a missing field, a bad token), and 5xx means something went wrong on our side. Errors come back as {"ok": false, "error": "…", "message": "…"}; validation errors return 422 and add a field-keyed errors object.
{
"ok": false,
"error": "ValidationException",
"message": "The name field is required. (and 1 more error)",
"errors": {
"name": ["The name field is required."],
"price": ["The price must be a number."]
}
}
200OK201Created401Unauthenticated — bad or missing token403Forbidden — the record belongs to another store404Resource not found422Validation failed429Rate limited500Server errorPagination
Big lists are paginated with page. Products and orders also take per_page (default 50, max 250); customers and reviews return 50 per page. The page is Laravel's paginator inside data — the records are at data.data, with current_page, last_page and total beside them. Short lists (collections, categories, brands) come back as a plain array in data.
{
"ok": true,
"data": {
"current_page": 2,
"data": [ /* … 25 products … */ ],
"per_page": 25,
"total": 214,
"last_page": 9,
"from": 26, "to": 50
}
}
Products
Create, read, update and delete products, including variants, images, pricing and inventory. Products belong to the store tied to your token.
/ecommerce/products/ecommerce/products/ecommerce/products/{id}/ecommerce/products/{id}/ecommerce/products/{id}Create a product
curl -X POST https://storevero.com/api/v1/ecommerce/products \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Aroma Diffuser",
"price": 1499,
"sku": "AD-001",
"stock": 50,
"category_id": 3,
"is_active": true
}'
await fetch('https://storevero.com/api/v1/ecommerce/products', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Aroma Diffuser', price: 1499,
sku: 'AD-001', stock: 50, category_id: 3, is_active: true
})
});
Http::withToken($token)->post(
'https://storevero.com/api/v1/ecommerce/products', [
'name' => 'Aroma Diffuser', 'price' => 1499,
'sku' => 'AD-001', 'stock' => 50, 'category_id' => 3, 'is_active' => true,
]);
Response
{
"ok": true,
"data": {
"id": 41, "workspace_id": 12, "name": "Aroma Diffuser", "slug": "aroma-diffuser",
"price": "1499.00", "sku": "AD-001", "stock": 50,
"category_id": 3, "is_active": true,
"created_at": "2026-07-29T10:20:31.000000Z",
"variants": []
}
}
Collections & categories
Organise your catalogue with collections, categories and brands. Creating any of them needs only a name; the lists come back unpaginated.
/ecommerce/collections/ecommerce/collections/ecommerce/categories/ecommerce/categories/ecommerce/brands/ecommerce/brandsOrders
Read and filter orders, record fulfilments and refunds, and export to CSV. No endpoint moves money: a refund is recorded on the order (amounts, restock, status); return the payment itself from your payment provider's dashboard.
/orders/orders/{id}/orders/{id}/ecommerce/fulfillments/orders/{id}/fulfill/shopify/refunds/orders/{id}/refund/orders/exportFulfil an order
Creates a fulfilment, marks the order fulfilled once every line has shipped, and sends the fulfillments/create and orders/fulfilled webhooks.
curl -X POST https://storevero.com/api/v1/ecommerce/fulfillments \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"order_id": 1042,
"line_items": [{ "order_item_id": 311, "quantity": 1 }],
"carrier": "Delhivery",
"tracking_number": "IN123456789",
"tracking_url": "https://www.delhivery.com/track/package/IN123456789"
}'
await fetch('https://storevero.com/api/v1/ecommerce/fulfillments', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: 1042,
line_items: [{ order_item_id: 311, quantity: 1 }],
carrier: 'Delhivery', tracking_number: 'IN123456789'
})
});
Http::withToken($token)->post(
'https://storevero.com/api/v1/ecommerce/fulfillments', [
'order_id' => 1042,
'line_items' => [['order_item_id' => 311, 'quantity' => 1]],
'carrier' => 'Delhivery', 'tracking_number' => 'IN123456789',
]);
Customers
Look up customers, their order history and lifetime value, and honour privacy requests with a one-call GDPR export.
/customers/customers/{id}/customers/{id}/customers/{id}/orders/customers/{id}/gdpr-exportInventory
Adjust stock with an auditable movement. You send a delta (positive or negative) and a reason; storeVero records the movement and updates the on-hand balance.
/ecommerce/inventory/adjustcurl -X POST https://storevero.com/api/v1/ecommerce/inventory/adjust \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "product_id": 41, "quantity": -3, "reason": "sale" }'
await fetch('https://storevero.com/api/v1/ecommerce/inventory/adjust', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: 41, quantity: -3, reason: 'sale' })
});
Http::withToken($token)->post(
'https://storevero.com/api/v1/ecommerce/inventory/adjust', [
'product_id' => 41, 'quantity' => -3, 'reason' => 'sale',
]);
reason is one of purchase, sale, return, adjustment or transfer.
Reviews
Moderate product reviews. Reviews submitted on the storefront arrive unapproved (see the Storefront API); approve one with {"is_approved": true}, answer with {"reply": "…"}, or remove it.
/ecommerce/reviews/ecommerce/reviews/{id}/ecommerce/reviews/{id}Bulk operations
Apply one action to many records in a single call. Send the ids, an action and, where the action needs one, a value; the response says how many records changed ({"affected": 12}), not a per-row result. To create products in bulk, use the import endpoint.
/bulk/products/bulk/orders/bulk/customers/ecommerce/products/bulk-import| Endpoint | Actions |
|---|---|
| /bulk/products | set_price, adjust_price_percent, activate, deactivate, add_tags, remove_tags, set_category, delete |
| /bulk/orders | set_status, mark_paid, archive, delete |
| /bulk/customers | add_tags, set_group |
curl -X POST https://storevero.com/api/v1/bulk/products \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "product_ids": [41, 42, 43], "action": "adjust_price_percent", "value": -10 }'
Storefront REST
Public, store-scoped endpoints for building custom storefronts, mobile apps and headless experiences. Every route is prefixed with the store handle. No admin token required.
/…/{store}/products/…/{store}/products/{slug}/…/{store}/categories/…/{store}/search?q=/…/{store}/autocomplete?q=/…/{store}/delivery-options?zip=/…/{store}/reviews/{product id}/…/{store}/reviews/…/{store}/ordersPlace an order
Required: email, first_name, address1 and items (each with product_id, quantity and, for products with options, variant_id). Address fields are flat. payment_method defaults to cod; a method the store has not set up is refused with 422 and the list of methods it can take.
curl -X POST https://storevero.com/api/v1/ecommerce/storefront/demo/orders \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"email": "buyer@example.com",
"first_name": "Asha", "last_name": "Kumar", "phone": "9800000000",
"address1": "12 MG Road", "city": "Pune", "state": "Maharashtra", "zip": "411001", "country": "IN",
"items": [{ "product_id": 41, "quantity": 2 }],
"payment_method": "cod"
}'
await fetch('https://storevero.com/api/v1/ecommerce/storefront/demo/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
email: 'buyer@example.com',
first_name: 'Asha', last_name: 'Kumar', phone: '9800000000',
address1: '12 MG Road', city: 'Pune', state: 'Maharashtra', zip: '411001', country: 'IN',
items: [{ product_id: 41, quantity: 2 }],
payment_method: 'cod'
})
});
Http::acceptJson()->post('https://storevero.com/api/v1/ecommerce/storefront/demo/orders', [
'email' => 'buyer@example.com',
'first_name' => 'Asha', 'last_name' => 'Kumar', 'phone' => '9800000000',
'address1' => '12 MG Road', 'city' => 'Pune', 'state' => 'Maharashtra', 'zip' => '411001', 'country' => 'IN',
'items' => [['product_id' => 41, 'quantity' => 2]],
'payment_method' => 'cod',
]);
Storefront GraphQL
A Shopify-shaped GraphQL Storefront API — edges, nodes and cart mutations — so tools and snippets from the Shopify ecosystem port over with minimal changes. Authenticate with a storefront access token.
/storefront/graphqlquery ProductByHandle($handle: String!) {
productByHandle(handle: $handle) {
id
title
handle
priceRange { minVariantPrice { amount currencyCode } }
images(first: 3) { edges { node { url altText } } }
}
}
curl -X POST https://storevero.com/api/v1/storefront/graphql \
-H "X-Shopify-Storefront-Access-Token: YOUR_STOREFRONT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "query": "{ productByHandle(handle: \"aroma-diffuser\") { title } }" }'
mutation {
cartCreate(input: { lines: [{ merchandiseId: "41", quantity: 2 }] }) {
cart { id checkoutUrl
lines(first: 10) { edges { node { quantity } } }
}
}
}
Webhooks
Add an endpoint in Settings → Webhooks (or POST /shopify/webhooks with topic, address and secret) and storeVero will POST a JSON payload to it when the event happens. When the webhook has a secret, every delivery carries an X-StoreVero-Hmac-Sha256 header — the base64 HMAC-SHA256 of the raw request body. Verify it before trusting the body. Webhooks saved without a secret are not signed.
Events you can subscribe to
orders/createOrder createdorders/paidOrder paidorders/updatedOrder editedorders/fulfilledOrder fulfilledorders/refundedReturn requestedfulfillments/createFulfilment createdfulfillments/updateFulfilment updatedrefunds/createRefund createddraft_orders/createDraft order createddraft_orders/updateDraft order updateddraft_orders/completedDraft order completedWhat a delivery looks like
Content-Type: application/json
X-StoreVero-Topic: orders/create
X-StoreVero-Event-Id: 5f0c1a9e-…
X-StoreVero-Hmac-Sha256: 3q2+7w…= (only when a secret is set)
{
"id": "5f0c1a9e-…",
"topic": "orders/create",
"created_at": "2026-09-15T10:20:31+00:00",
"workspace": 12,
"data": { /* the order, fulfilment, refund or draft order */ }
}
Each event is sent once, with a 10-second timeout; failed deliveries are not retried automatically. Every attempt, with the HTTP status your endpoint returned, is listed under Logs in Settings → Webhooks, and Send test posts a sample delivery to one endpoint.
Verify a signature
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_STOREVERO_HMAC_SHA256'] ?? '';
$expected = base64_encode(hash_hmac('sha256', $payload, $webhookSecret, true));
if (! hash_equals($expected, $signature)) {
http_response_code(401); exit;
}
// ✓ verified — parse and handle
$event = json_decode($payload, true);
import crypto from 'crypto';
// rawBody: the exact bytes received (not re-serialised JSON)
function verify(rawBody, signature, secret) {
const expected = Buffer.from(crypto.createHmac('sha256', secret)
.update(rawBody).digest('base64'));
const given = Buffer.from(signature || '');
return given.length === expected.length && crypto.timingSafeEqual(expected, given);
}
SDKs & libraries
There are no official SDKs or Postman collection yet. The API is plain HTTP + JSON, so any HTTP client works — every cURL, JavaScript (fetch) and PHP (Laravel Http) sample on this page is a complete request you can copy.
Changelog
Ready to build?
Create a free store, generate an API key, and make your first call in minutes.
Get your API key