Getting Started
Overview
The Finxa Admin API is a RESTful API that lets you programmatically manage your store's products, orders, customers, collections, discounts, shipping, and more. Build custom integrations, automate workflows, or connect your store to third-party services.
All API endpoints are available at:
https://api.finxa.store/apiEvery response follows a consistent JSON envelope:
{
"success": true,
"data": { ... },
"message": "Product created"
}{
"success": false,
"error": "Product not found"
}Authentication
To use the API, you need a Custom App. Create one from your Finxa dashboard under Settings → Apps.
Step 1: Create an App
Navigate to Dashboard → Apps → Create app. Give it a name and select the API scopes you need.
Step 2: Install & Get Credentials
Click "Install app" to activate it. You'll receive two credentials:
| Parameter | Type | Description |
|---|---|---|
API Key | string | Public identifier for your app. Starts with fxk_. Safe to store in config. |
Secret Key | string | Private access token. Starts with fxat_. Treat it like a password — never commit to version control. |
Step 3: Authenticate Requests
Include your secret key in the X-Finxa-Access-Token header:
curl https://api.finxa.store/api/products \
-H "X-Finxa-Access-Token: fxat_your_secret_key_here"fxk_) is your app's public identifier. The secret key (fxat_) is what you send in the header. Don't confuse them.Making Requests
All request and response bodies use JSON. Set the Content-Type header for requests that include a body:
curl -X POST https://api.finxa.store/api/products \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Premium T-Shirt",
"status": "ACTIVE",
"variants": [
{ "title": "Default", "price": 29.99, "inventory": 100 }
]
}'Response Format
Every response is a JSON object with a success boolean. Successful responses include a data field. Error responses include an error string.
| Parameter | Type | Description |
|---|---|---|
successRequired | boolean | Whether the request succeeded. |
data | object | array | The response payload. Present on success. |
message | string | A human-readable success message. Not always present. |
error | string | Error description. Present on failure. |
Paginated Responses
List endpoints support pagination via query parameters:
| Parameter | Type | Description |
|---|---|---|
page | number | Page number (1-indexed). Default: 1 |
limit | number | Items per page. Default: 20, max: 100 |
{
"success": true,
"data": {
"products": [ ... ],
"pagination": {
"total": 156,
"page": 1,
"limit": 20,
"totalPages": 8
}
}
}Rate Limits
Each app token gets its own budget, so one busy integration never slows the dashboard or another app:
| Parameter | Type | Description |
|---|---|---|
App tokens | limit | 300 requests per minute per app, with X-RateLimit-Limit / X-RateLimit-Remaining on every response |
Storefront tokens | limit | 300 requests per minute per client IP on /api/storefront |
When rate limited you receive 429 Too Many Requests with a Retry-After header (seconds). Back off and retry after that.
Errors
The API uses standard HTTP status codes:
| Parameter | Type | Description |
|---|---|---|
200 | OK | Request succeeded. |
201 | Created | Resource created successfully. |
400 | Bad Request | Invalid request body or parameters. |
401 | Unauthorized | Missing or invalid access token. |
403 | Forbidden | Insufficient scopes for this action. |
404 | Not Found | Resource doesn't exist or doesn't belong to your store. |
409 | Conflict | Resource conflict (duplicate email, can't delete with related records). |
429 | Rate Limited | Too many requests. Retry after a short delay. |
500 | Internal Error | Something went wrong on our side. Contact support. |
{
"success": false,
"error": "Insufficient scopes. Required: write_products"
}Webhooks
Webhooks push events to your endpoint the moment something changes in the store — a new order, a product edit, stock going down at the POS — so you never have to poll. Each delivery is signed, retried when your endpoint is down, and logged so both you and the merchant can see what happened.
Topics
| Parameter | Type | Description |
|---|---|---|
orders/create | read_orders | An order was placed (online checkout, POS sale, or a completed draft). |
orders/paid | read_orders | Payment for an order was confirmed. |
orders/updated | read_orders | Anything on an order changed: status, items, addresses, notes, tracking, refunds. |
orders/fulfilled | read_orders | An order was marked fulfilled (tracking details included when set). |
orders/cancelled | read_orders | An order was cancelled by the merchant or because payment failed. |
refunds/create | read_orders | A refund was recorded against an order. |
products/create | read_products | A product was created (including duplicates and CSV imports). |
products/update | read_products | A product, one of its variants, or its images changed. |
products/delete | read_products | A product was deleted. |
collections/create | read_collections | A collection was created. |
collections/update | read_collections | A collection changed. |
collections/delete | read_collections | A collection was deleted. |
inventory_levels/update | read_products | A tracked variant's available quantity changed (orders, returns, manual adjustments, POS sales). |
customers/create | read_customers | A customer was created. |
customers/update | read_customers | A customer's profile changed. |
customers/delete | read_customers | A customer was deleted. |
app/uninstalled | — | The merchant revoked this app. Sent to every webhook the app had, then the app's tokens stop working. |
The third column is the scope the app must hold to subscribe to that topic.
Envelope & headers
Every delivery is a POST with a JSON envelope. `data` carries the resource in the same shape the admin API returns it (orders/* carry the order as GET /api/orders/:id does; products/* the product; inventory_levels/update an inventory_level object; refunds/create the refund and its order).
{
"id": "evt_9f1c2b7a4d3e8f10aa11bb22",
"topic": "orders/create",
"api_version": "2026-09",
"created_at": "2026-09-18T06:20:11.000Z",
"store_id": "cmu6e4ndz0001…",
"data": {
"order": { "id": "cmu6e4nxq005k…", "orderNumber": 1042, "status": "PAID", "total": "24.690", "items": [ … ] }
}
}Headers
| Parameter | Type | Description |
|---|---|---|
X-Finxa-Topic | header | The topic, e.g. orders/create |
X-Finxa-Hmac-SHA256 | header | Base64 HMAC-SHA256 of the raw request body, keyed with the webhook's secret |
X-Finxa-Event-Id | header | Event id — identical across every webhook that receives the same event, and across retries |
X-Finxa-Delivery-Id | header | This delivery attempt's row id (shown in the dashboard log) |
X-Finxa-Webhook-Id | header | The subscription id |
X-Finxa-Api-Version | header | Envelope/payload version |
X-Finxa-Test | header | Present ("true") only for test deliveries sent from the dashboard |
Verifying signatures
Compute the HMAC over the raw bytes you received — before any JSON parsing or re-serialisation — and compare it in constant time. Respond 2xx within 10 seconds; do the real work asynchronously.
import { createHmac, timingSafeEqual } from "node:crypto";
app.post("/webhooks/finxa", express.raw({ type: "application/json" }), (req, res) => {
const expected = createHmac("sha256", process.env.FINXA_WEBHOOK_SECRET)
.update(req.body) // the raw bytes, not a re-serialised object
.digest("base64");
const given = req.get("X-Finxa-Hmac-SHA256") ?? "";
if (given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
// Store event.id and skip duplicates — retries redeliver the same id.
res.status(200).end(); // answer fast; do the work in a queue
});$raw = file_get_contents('php://input');
$expected = base64_encode(hash_hmac('sha256', $raw, $_ENV['FINXA_WEBHOOK_SECRET'], true));
if (!hash_equals($expected, $_SERVER['HTTP_X_FINXA_HMAC_SHA256'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);Retries & delivery log
A delivery counts as successful on any 2xx. Anything else (a 4xx/5xx, a timeout after 10 s, a connection error) is retried on this schedule, about 8.6 hours in total, after which the delivery is marked failed and can be redelivered from the dashboard or the API:
| Parameter | Type | Description |
|---|---|---|
1 | attempt | immediately |
2 | attempt | after 1 minute |
3 | attempt | 5 minutes after that |
4 | attempt | 30 minutes after that |
5 | attempt | 2 hours after that |
6 | attempt | 6 hours after that (last) |
Registering from your app
An installed app registers its own subscriptions with its access token — the merchant never opens the dashboard for it. This is how a public app sets itself up on each store it is installed on. Reads need read_webhooks, writes need write_webhooks, and each topic also needs its own scope.
/api/appThe calling app and its store — works with any valid app token.
/api/app/topicsAll topics with descriptions and required scopes
/api/app/webhooksThe app's own subscriptions.
read_webhooks/api/app/webhooksSubscribe to a topic. Returns the signing secret once; 409 topic_already_subscribed when the app already has that topic.
write_webhooks/api/app/webhooks/{webhookId}Change the address or topic, or pause it with isActive: false.
write_webhooks/api/app/webhooks/{webhookId}Unsubscribe.
write_webhooks/api/app/webhooks/{webhookId}/rotate-secretNew signing secret, shown once. Deliveries are signed with it immediately.
write_webhooks/api/app/webhooks/{webhookId}/testSend a signed test event so you can verify your receiver.
write_webhooks/api/app/deliveriesDelivery log across the app's subscriptions, newest first (limit, cursor).
read_webhooks/api/app/deliveries/{deliveryId}/redeliverQueue a failed delivery again.
write_webhooks# On install, register the events your app needs — no dashboard visit required.
curl -X POST https://api.finxa.store/api/app/webhooks \
-H "X-Finxa-Access-Token: fxat_…" \
-H "Content-Type: application/json" \
-d '{ "topic": "orders/paid", "address": "https://yourapp.com/hooks/finxa" }'
# 201 — the signing secret is in the response and is never shown again.
{ "success": true,
"data": { "id": "…", "topic": "orders/paid", "address": "https://yourapp.com/hooks/finxa",
"isActive": true, "secret": "9f86d0818…" },
"warning": "Save the secret now — it signs every delivery (X-Finxa-Hmac-SHA256) and is only shown once." }
# Already subscribed to that topic? 409 with the existing id, so you can update it instead.
{ "success": false, "code": "topic_already_subscribed", "webhookId": "…" }Managing subscriptions
Subscriptions belong to an app and are managed with the merchant's dashboard session (the same calls the Apps page makes). One subscription per topic per app.
/api/apps/topicsAll topics with descriptions and required scopes
/api/apps/{storeId}/{appId}/webhooksSubscribe { topic, address } — returns the signing secret once
/api/apps/{storeId}/{appId}/webhooks/{webhookId}Change address/topic or toggle isActive
/api/apps/{storeId}/{appId}/webhooks/{webhookId}/testSend a synthetic event now and return the recorded attempt
/api/apps/{storeId}/{appId}/webhooks/{webhookId}/deliveriesDelivery log (status, attempts, last status code, error)
/api/apps/{storeId}/{appId}/deliveries/{deliveryId}/redeliverQueue a fresh round of attempts for one delivery
Rate limits, idempotency & versions
Three things every app-token request gets for free: a per-app rate limit, idempotency keys for safe retries, and a dated API version you can pin.
Rate limiting
300 requests per minute per app. Every response tells you where you stand; a 429 tells you how long to wait.
| Parameter | Type | Description |
|---|---|---|
X-RateLimit-Limit | header | Requests allowed per minute |
X-RateLimit-Remaining | header | Requests left in the current minute |
Retry-After | header | On 429 — seconds to wait before retrying |
Idempotency keys
Send Idempotency-Key (any string up to 255 characters, unique per operation) on POST/PUT/PATCH/DELETE. If the same key arrives again within 24 hours for the same app, method and path, the stored response is returned with Idempotent-Replayed: true and the handler does not run again. A repeat that arrives while the first request is still running gets 409.
curl -X POST https://api.finxa.store/api/orders/draft \
-H "X-Finxa-Access-Token: fxat_…" \
-H "Idempotency-Key: order-sync-8842" \
-H "Content-Type: application/json" \
-d '{ "items": [{ "variantId": "cmu…", "quantity": 1 }], "customerEmail": "[email protected]" }'
# Same key again within 24 h → the stored response, plus:
# Idempotent-Replayed: trueAPI versioning
API versions are dates. Send X-Finxa-API-Version to pin one; omit it to get the current version. Every response echoes the version it was served with. When a breaking change ships it comes as a new dated version and the previous one keeps working for at least twelve months. Unknown versions are rejected with 400.
curl https://api.finxa.store/api/products \
-H "X-Finxa-Access-Token: fxat_…" \
-H "X-Finxa-API-Version: 2026-09"
# Every response echoes the version it was served with:
# X-Finxa-API-Version: 2026-09Request log
Every app-token request is logged (method, path, status, duration, IP) and kept for 30 days. Merchants see the count on the app page; the log is available at GET /api/apps/{storeId}/{appId}/logs with a dashboard session.
OpenAPI
A machine-readable OpenAPI 3.1 description of every endpoint, built from the live route table, for client generators and API tools:
https://api.finxa.store/api/openapi.jsonStorefront API (headless)
Build your own storefront — a custom Next.js site, a mobile app, a kiosk — on the same catalog, carts and checkout the hosted storefront uses. Read the catalog, build a cart, then hand the shopper to the hosted checkout, which already handles payments, markets, taxes and phone validation.
Storefront tokens
Send the token as X-Finxa-Storefront-Token (or Authorization: Bearer fxst_…). CORS is open on this prefix, so browsers can call it directly. Requests are limited to 300 per minute per client IP.
curl "https://api.finxa.store/api/storefront/products?limit=12&sort=created-desc" \
-H "X-Finxa-Storefront-Token: fxst_…"Catalog
/api/storefront/shopStore name, currency, language, public URL and active markets
/api/storefront/productsActive products, newest first
/api/storefront/products/{handle}One product by handle or id
/api/storefront/search?q=Search by title, description, vendor or tag
/api/storefront/collectionsPublished collections
/api/storefront/collections/{handle}/productsProducts in a collection (same filters as /products)
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | number | 1–100, default 24 |
cursor | string | next_cursor from the previous page |
q | string | Free-text search |
collection | string | Collection handle |
tag | string | Exact tag |
sort | string | created-desc (default), created-asc, title-asc, title-desc |
{
"success": true,
"data": [
{
"id": "cmu…", "title": "Racing Polo", "handle": "racing-polo",
"available": true,
"price_range": { "min": { "amount": "12.500", "currency": "BHD" }, "max": { "amount": "12.500", "currency": "BHD" } },
"images": [{ "src": "https://…/polo.jpg", "alt": "Racing Polo", "width": 1600, "height": 1600 }],
"options": [{ "name": "Size", "values": ["S", "M", "L"] }],
"variants": [{ "id": "cmu…", "title": "S", "price": { "amount": "12.500", "currency": "BHD" }, "available": true, "options": { "Size": "S" } }]
}
],
"pagination": { "limit": 12, "has_more": true, "next_cursor": "cmu…" }
}Carts
Carts live on the server for 7 days. Every cart response includes totals in the store currency and a checkout_url. Quantities are clamped to what can actually be bought; lines whose product stops being sold disappear.
/api/storefront/cartsCreate a cart, optionally with lines
/api/storefront/carts/{id}Cart with totals and checkout_url
/api/storefront/carts/{id}/itemsAdd a line (merges with an existing line for the same variant)
/api/storefront/carts/{id}/items/{variantId}Set a line's quantity; 0 removes it
/api/storefront/carts/{id}/items/{variantId}Remove a line
/api/storefront/carts/{id}/checkoutReturns checkout_url for the hosted checkout
// 1. create a cart with a first line
const cart = await fetch("https://api.finxa.store/api/storefront/carts", {
method: "POST",
headers: { "X-Finxa-Storefront-Token": TOKEN, "Content-Type": "application/json" },
body: JSON.stringify({ items: [{ variantId: "cmu…", quantity: 2 }] }),
}).then((r) => r.json());
// 2. add / change lines as the shopper browses
await fetch(`https://api.finxa.store/api/storefront/carts/${cart.data.id}/items`, {
method: "POST", headers, body: JSON.stringify({ variantId: "cmu…", quantity: 1 }),
});
// 3. send the shopper to the hosted checkout
window.location.href = cart.data.checkout_url;
// → https://<your-store-domain>/cart/load?cart=…&token=… → /checkoutDiscount codes
A shopper's code is validated against the same rules as the hosted checkout (minimum amount, product or collection scope, usage limits). The cart then carries the code: totals include it, and the hosted checkout applies it automatically after the handoff.
/api/storefront/carts/{id}/discountApply a code — 400 with code discount_invalid when it does not apply; the cart response includes discount and total.
/api/storefront/carts/{id}/discountRemove the applied code.
// POST /api/storefront/carts/{id}/discount { "code": "TEN" }
{
"success": true,
"data": {
"id": "cart_…",
"subtotal": { "amount": "45.000", "currency": "BHD" },
"discount": { "code": "TEN", "title": "Ten off", "type": "PERCENTAGE", "value": 10,
"amount": { "amount": "4.500", "currency": "BHD" }, "free_shipping": false },
"discount_error": null,
"total": { "amount": "40.500", "currency": "BHD" },
"checkout_url": "https://shop.example.com/cart/load?cart=…&token=…"
}
}Shipping rates
Rates for a destination country, computed from the store's zones and rules. Pass a cart_id to derive the subtotal and items, or send subtotal yourself. Optional market_id and locale (for translated rate names).
/api/storefront/shipping/ratesRates for country_code — { country_code, cart_id? | subtotal?, market_id?, locale? } → data.rates[].
curl -X POST https://api.finxa.store/api/storefront/shipping/rates \
-H "X-Finxa-Storefront-Token: fxst_…" -H "Content-Type: application/json" \
-d '{ "country_code": "BH", "cart_id": "cart_…" }'
# or without a cart: { "country_code": "SA", "subtotal": 40.5, "market_id": "…", "locale": "ar" }Menus, pages & blog
Everything a headless site needs besides the catalog: navigation menus (resolved to links), CMS pages, the auto-generated policy pages, and the blog. Only published content is returned.
/api/storefront/menusMenus with their handle and item count.
/api/storefront/menus/{handle}A menu resolved to a tree of links (collections, products, pages, custom URLs).
/api/storefront/pagesPublished pages (without bodies; policy pages excluded).
/api/storefront/pages/{handle}One page by handle, with its HTML body.
/api/storefront/policiesThe store's policy pages: shipping, returns, contact, privacy, terms.
/api/storefront/blogPublished posts, newest first — limit, cursor, tag.
/api/storefront/blog/{handle}One post by handle, with its HTML body.
Customer accounts
Shoppers sign in with a one-time code sent to their e-mail — the same login the hosted storefront uses — and your site gets a customer token (fxct_…, 30 days) to send as X-Finxa-Customer-Token. Accounts are created on first sign-in.
/api/storefront/customers/login/requestSend a 6-digit code to { email } — 10-minute validity, at most 3 per minute; 422 email_undeliverable when the address bounces.
/api/storefront/customers/login/verifyExchange { email, code } for { customer_token, expires_at, customer } — 401 invalid_code after 5 wrong attempts.
/api/storefront/customers/meThe signed-in customer: profile and addresses.
/api/storefront/customers/meUpdate first_name, last_name, phone, locale.
/api/storefront/customers/me/ordersThe customer's orders (by customer id or e-mail), newest first, with items and tracking — limit, cursor.
/api/storefront/customers/logoutInvalidate the customer token.
// 1. ask for a code — the shopper gets a 6-digit code by e-mail (10 min, 3 per minute)
await sf("/customers/login/request", { method: "POST", body: JSON.stringify({ email }) });
// 2. exchange the code for a customer token (30 days)
const { data } = await (await sf("/customers/login/verify", {
method: "POST", body: JSON.stringify({ email, code }) })).json();
localStorage.setItem("customer_token", data.customer_token); // fxct_…
// 3. call account endpoints with the token
const me = await sf("/customers/me", { headers: { "X-Finxa-Customer-Token": data.customer_token } });
const orders = await sf("/customers/me/orders?limit=20", { headers: { "X-Finxa-Customer-Token": data.customer_token } });Checkout handoff
checkout_url points at the store's own domain: /cart/load?cart=…&token=…. That route copies the cart into the hosted storefront's cart cookie and redirects to /checkout, so the shopper pays through the merchant's configured gateways (TAP, Stripe, PayPal, Checkout.com, cash on delivery) with the merchant's shipping rates and taxes. Orders placed this way trigger orders/create like any other order.
OAuth (public apps)
A public app is built once and installed by many merchants. The merchant approves it on a consent screen; your app receives a store-scoped access token that works on the admin API exactly like a custom-app token, until the merchant uninstalls.
Register an app
Register the app from Dashboard → Apps → Public apps (for developers), or with the API below. You get a client_id and a client_secret; declare the redirect URIs (exact match, https) and the widest set of scopes the app may ever request.
/api/public-apps{ name, redirectUris[], scopes[] } → client_id + client_secret (once)
Authorization flow
| Parameter | Type | Description |
|---|---|---|
1 | step | Send the merchant to /api/oauth/authorize with client_id, the scopes you need, a registered redirect_uri and a random state. |
2 | step | Finxa validates the request and shows the merchant a consent screen in their dashboard (they sign in if needed). Requests with an unregistered redirect_uri are rejected outright; a scope outside your registration comes back to you as error=invalid_scope. |
3 | step | On approval the merchant is redirected to your redirect_uri with ?code=…&state=…. Check state, then exchange the code within 10 minutes — it is single-use. |
4 | step | POST /api/oauth/token with the code, your client credentials and the same redirect_uri. Store the returned access_token per store. |
https://api.finxa.store/api/oauth/authorize
?client_id=fxca_…
&scope=read_orders,write_products
&redirect_uri=https://yourapp.com/oauth/callback
&state=8f3a… # random, checked on return
&code_challenge=… # optional PKCE (S256)
&code_challenge_method=S256curl -X POST https://api.finxa.store/api/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "<code from the callback>",
"client_id": "fxca_…",
"client_secret": "fxcs_…",
"redirect_uri": "https://yourapp.com/oauth/callback"
}'
# {
# "access_token": "fxat_…",
# "token_type": "bearer",
# "scope": "read_orders write_products",
# "store": { "id": "cmu…", "handle": "sunglasses-bhr", "name": "Sunglasses BHR", "currency": "BHD" },
# "app_id": "cmu…",
# "api_version": "2026-09"
# }Token errors follow OAuth: invalid_client (401) for a wrong id/secret, invalid_grant (400) for a used, expired or mismatched code, invalid_request (400) for a malformed body. PKCE (S256) is supported: send code_challenge on the authorize URL and code_verifier on the token exchange.
Access tokens
Access tokens are permanent offline tokens: they keep working until the merchant uninstalls the app (or your app uninstalls itself), and they carry exactly the scopes the merchant approved. Re-authorizing a store updates its scopes and issues a fresh token; the previous one stops working.
/api/oauth/meWhich store and app this token belongs to
/api/oauth/installs/currentUninstall from the store: revokes the token and sends app/uninstalled
Refresh tokens & expiry
By default access tokens are permanent. If your app sets an access-token lifetime (Apps page → Listing & tokens, or accessTokenTtlSeconds on PUT /api/public-apps/{id}), the token response carries expires_in and a refresh_token. Refresh tokens are single-use and rotate on every refresh; they last a year.
# token response when the app has an access-token lifetime set
{ "access_token": "fxat_…", "token_type": "bearer", "expires_in": 3600,
"refresh_token": "fxrt_…", "scope": "read_orders write_products", "store": { … }, "app_id": "…" }
# before expires_in runs out (or on a 401): rotate both tokens
curl -X POST https://api.finxa.store/api/oauth/token \
-H "Content-Type: application/json" \
-d '{ "grant_type": "refresh_token", "refresh_token": "fxrt_…",
"client_id": "fxca_…", "client_secret": "fxcs_…" }'
# → a new access_token + refresh_token; the old pair stops working immediatelyApp directory listing
Merchants see listed apps on their Apps page with an Install button. Set an install URL on your side that starts the authorization flow, then turn on the listing. Logo, category, support e-mail and description make the card.
/api/public-apps/{id}Update name, description, redirect URIs, scopes, listing fields and the access-token lifetime.
PUT /api/public-apps/{id}
{
"listed": true,
"installUrl": "https://yourapp.com/finxa/install", // starts your OAuth flow
"logoUrl": "https://yourapp.com/logo.png",
"category": "shipping",
"supportEmail": "[email protected]",
"description": "Prints labels and syncs tracking numbers.",
"accessTokenTtlSeconds": null // or e.g. 3600 for expiring tokens
}Products
Products represent items in your store. Every product has at least one variant (even simple products). Variants hold the price, inventory, SKU, and options like size/color.
List Products
/api/productsRetrieve a paginated list of products.
read_productsQuery Parameters
| Parameter | Type | Description |
|---|---|---|
page | number | Page number. Default: 1 |
limit | number | Items per page (max 100). Default: 20 |
status | string | Filter by status: DRAFT, ACTIVE, or ARCHIVED |
search | string | Search by title, handle, vendor, type, variant title, SKU, or barcode |
curl "https://api.finxa.store/api/products?status=ACTIVE&limit=10" \
-H "X-Finxa-Access-Token: fxat_your_secret_key"{
"success": true,
"data": {
"products": [
{
"id": "cmmm0ekmc0000ny1p7n1vdsbf",
"title": "Premium T-Shirt",
"handle": "premium-t-shirt",
"status": "ACTIVE",
"description": "A comfortable cotton t-shirt",
"vendor": "Finxa Apparel",
"productType": "Clothing",
"tags": ["summer", "cotton"],
"images": [],
"options": [
{ "name": "Size", "position": 0, "values": ["S", "M", "L"] }
],
"weight": null,
"weightUnit": "kg",
"isPhysical": true,
"seoTitle": null,
"seoDescription": null,
"publishedAt": "2026-03-10T12:00:00.000Z",
"createdAt": "2026-03-10T12:00:00.000Z",
"updatedAt": "2026-03-10T12:30:00.000Z",
"variants": [
{
"id": "clxyz123",
"title": "Small",
"sku": "TS-S",
"barcode": null,
"price": "29.990",
"costPrice": "12.000",
"compareAtPrice": null,
"inventory": 50,
"trackInventory": true,
"taxable": true,
"requiresShipping": true,
"options": { "Size": "S" },
"position": 0,
"weight": null,
"weightUnit": "kg"
}
],
"productImages": [],
"_count": { "collections": 1 }
}
],
"pagination": {
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5
}
}
}Get Product
/api/products/:productIdRetrieve a single product with all variants, images, and collection associations.
read_productscurl "https://api.finxa.store/api/products/cmmm0ekmc0000ny1p7n1vdsbf" \
-H "X-Finxa-Access-Token: fxat_your_secret_key"Create Product
/api/productsCreate a new product with at least one variant.
write_productsRequest Body
| Parameter | Type | Description |
|---|---|---|
titleRequired | string | Product title (1-255 chars) |
description | string | Plain text description |
descriptionHtml | string | Rich HTML description |
handle | string | URL slug (auto-generated from title if omitted). Lowercase, numbers, and hyphens only. |
status | string | DRAFT (default), ACTIVE, or ARCHIVED |
vendor | string | Product vendor/brand name |
productType | string | Product type/category |
tags | string[] | Array of tag strings |
images | string[] | Array of image URLs |
options | object[] | Product options, e.g. [{ "name": "Size", "values": ["S","M","L"] }] |
weight | number | Product weight |
weightUnit | string | Weight unit: kg (default), g, lb, oz |
isPhysical | boolean | Whether product requires shipping. Default: true |
seoTitle | string | SEO title (max 70 chars) |
seoDescription | string | SEO description (max 320 chars) |
variantsRequired | object[] | At least one variant (see Variant Object below) |
Variant Object
| Parameter | Type | Description |
|---|---|---|
titleRequired | string | Variant title, e.g. "Small" or "Red / Large" |
priceRequired | number | Price (≥ 0) |
sku | string | Stock keeping unit |
barcode | string | Barcode (UPC, EAN, etc.) |
costPrice | number | Cost of goods for profit tracking |
compareAtPrice | number | Original price for sale display |
inventory | number | Stock quantity. Default: 0 |
trackInventory | boolean | Track stock levels. Default: true |
taxable | boolean | Charge tax on this variant. Default: true |
requiresShipping | boolean | Requires shipping. Default: true |
options | object | Key-value pairs, e.g. { "Size": "S", "Color": "Red" } |
image | string | Variant-specific image URL |
position | number | Sort position. Default: 0 |
weight | number | Per-variant weight (overrides product weight) |
weightUnit | string | Weight unit override |
curl -X POST "https://api.finxa.store/api/products" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Classic Polo Shirt",
"description": "A timeless polo in premium cotton.",
"status": "ACTIVE",
"vendor": "Finxa Apparel",
"productType": "Clothing",
"tags": ["polo", "cotton", "casual"],
"options": [
{ "name": "Size", "position": 0, "values": ["S", "M", "L", "XL"] }
],
"variants": [
{ "title": "Small", "price": 35.00, "sku": "POLO-S", "inventory": 25, "options": { "Size": "S" } },
{ "title": "Medium", "price": 35.00, "sku": "POLO-M", "inventory": 40, "options": { "Size": "M" } },
{ "title": "Large", "price": 35.00, "sku": "POLO-L", "inventory": 30, "options": { "Size": "L" } },
{ "title": "XL", "price": 37.50, "sku": "POLO-XL", "inventory": 15, "options": { "Size": "XL" } }
]
}'Update Product
/api/products/:productIdUpdate product fields. Only send the fields you want to change.
write_productscurl -X PATCH "https://api.finxa.store/api/products/cmmm0ekmc0000ny1p7n1vdsbf" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Premium Polo Shirt — Updated",
"tags": ["polo", "premium", "new-arrival"]
}'Delete Product
/api/products/:productIdPermanently delete a product and all its variants. Products with existing orders cannot be deleted — archive them instead.
write_productscurl -X DELETE "https://api.finxa.store/api/products/cmmm0ekmc0000ny1p7n1vdsbf" \
-H "X-Finxa-Access-Token: fxat_your_secret_key"409 Conflict. Use PATCH with {"status": "ARCHIVED"} instead.Add Variant
/api/products/:productId/variantsAdd a new variant to an existing product.
write_productscurl -X POST "https://api.finxa.store/api/products/cmmm0ekmc0000ny1p7n1vdsbf/variants" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "XXL",
"price": 39.99,
"sku": "POLO-XXL",
"inventory": 10,
"options": { "Size": "XXL" }
}'Update Variant
/api/products/:productId/variants/:variantIdUpdate an existing variant. Only send the fields you want to change.
write_productscurl -X PATCH "https://api.finxa.store/api/products/cmmm0ekmc0000ny1p7n1vdsbf/variants/clxyz123" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"price": 32.50,
"inventory": 75,
"costPrice": 14.00
}'Delete Variant
/api/products/:productId/variants/:variantIdDelete a variant. A product must always have at least one variant.
write_products400 Bad Request if you try.Bulk Sync Variants
/api/products/:productId/variants/syncSync all variants in one call. Send the complete desired state — the API will create new variants, update existing ones, and delete any that aren't in your list.
write_productsThis is an idempotent replace operation. Include an id on variants you want to update; omit it for new ones. Variants not in the list will be deleted.
curl -X PUT "https://api.finxa.store/api/products/cmmm0ekmc0000ny1p7n1vdsbf/variants/sync" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"variants": [
{ "id": "existing_id_1", "title": "Small", "price": 29.99, "inventory": 50 },
{ "id": "existing_id_2", "title": "Medium", "price": 29.99, "inventory": 40 },
{ "title": "Large", "price": 29.99, "inventory": 30 },
{ "title": "XL", "price": 34.99, "inventory": 20 }
]
}'Collections
Collections group products together. They can be manual (hand-picked products) or smart (auto-populated based on rules like tag, vendor, or price).
List Collections
/api/collectionsRetrieve all collections for your store.
read_collectionscurl "https://api.finxa.store/api/collections" \
-H "X-Finxa-Access-Token: fxat_your_secret_key"Get Collection
/api/collections/:collectionIdRetrieve a single collection with its products.
read_collectionsCreate Collection
/api/collectionsCreate a new collection.
write_collectionsRequest Body
| Parameter | Type | Description |
|---|---|---|
titleRequired | string | Collection title |
handle | string | URL slug (auto-generated if omitted) |
description | string | Plain text description |
descriptionHtml | string | Rich HTML description |
image | string | Collection image URL |
collectionType | string | MANUAL (default) or SMART |
sortOrder | string | MANUAL, BEST_SELLING, ALPHA_ASC, ALPHA_DESC, PRICE_ASC, PRICE_DESC, CREATED_ASC, CREATED_DESC |
seoTitle | string | SEO title |
seoDescription | string | SEO description |
curl -X POST "https://api.finxa.store/api/collections" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Summer Collection 2026",
"description": "Lightweight essentials for the summer season.",
"collectionType": "MANUAL",
"sortOrder": "MANUAL"
}'Update Collection
/api/collections/:collectionIdUpdate collection fields.
write_collectionsDelete Collection
/api/collections/:collectionIdDelete a collection. Products in the collection are not deleted.
write_collectionsAdd Product to Collection
/api/collections/:collectionId/productsAdd one or more products to a manual collection.
write_collectionscurl -X POST "https://api.finxa.store/api/collections/col_abc123/products" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"productIds": ["prod_id_1", "prod_id_2"]
}'Remove Product from Collection
/api/collections/:collectionId/products/:productIdRemove a product from a collection.
write_collectionsOrders
Orders represent customer purchases. Each order has line items, a status, payment information, and shipping details.
Order Statuses
| Parameter | Type | Description |
|---|---|---|
DRAFT | status | Draft order, not yet placed |
PENDING | status | Order placed, awaiting payment |
PAID | status | Payment received |
PROCESSING | status | Being prepared for shipment |
FULFILLED | status | Shipped to customer |
COMPLETED | status | Delivered and finalized |
CANCELLED | status | Order cancelled |
REFUNDED | status | Full refund issued |
PARTIALLY_REFUNDED | status | Partial refund issued |
List Orders
/api/ordersRetrieve a paginated list of orders.
read_ordersQuery Parameters
| Parameter | Type | Description |
|---|---|---|
page | number | Page number. Default: 1 |
limit | number | Items per page. Default: 20 |
status | string | Filter by order status |
curl "https://api.finxa.store/api/orders?status=PAID&limit=10" \
-H "X-Finxa-Access-Token: fxat_your_secret_key"Get Order
/api/orders/:idRetrieve a single order with all details.
read_ordersCreate Draft Order
/api/orders/draftCreate a draft order with line items.
write_orderscurl -X POST "https://api.finxa.store/api/orders/draft" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust_abc123",
"items": [
{ "variantId": "var_xyz789", "quantity": 2 }
],
"shippingAddress": {
"line1": "123 Main St",
"city": "Manama",
"country": "BH"
}
}'Update Order Status
/api/orders/:id/statusTransition an order to a new status.
write_orderscurl -X PATCH "https://api.finxa.store/api/orders/order_abc123/status" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{ "status": "FULFILLED" }'COMPLETED back to DRAFT.Complete Order
/api/orders/:id/completeMark a fulfilled order as completed.
write_ordersUpdate Order Items
/api/orders/:id/itemsReplace or adjust line items (exchange); inventory and totals update on save.
write_ordersUpdate Order Notes
/api/orders/:id/notesAdd or update internal notes on an order.
write_orderscurl -X PATCH "https://api.finxa.store/api/orders/order_abc123/notes" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{ "notes": "Customer requested gift wrapping" }'Update Order Shipping
/api/orders/:id/shippingAdd or update tracking information.
write_orderscurl -X PATCH "https://api.finxa.store/api/orders/order_abc123/shipping" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"trackingNumber": "1Z999AA10123456784",
"trackingUrl": "https://track.example.com/1Z999AA10123456784",
"trackingCarrier": "Aramex"
}'Customers
Customers represent people who have purchased from your store or that you've added manually. Each customer has a unique email per store.
List Customers
/api/customersRetrieve a paginated list of customers with order counts and total spent.
read_customersQuery Parameters
| Parameter | Type | Description |
|---|---|---|
page | number | Page number. Default: 1 |
limit | number | Items per page (max 100). Default: 20 |
search | string | Search by email, first name, last name, or phone |
Get Customer
/api/customers/:idRetrieve a customer with their order history and total spent.
read_customersCreate Customer
/api/customersCreate a new customer.
write_customersRequest Body
| Parameter | Type | Description |
|---|---|---|
emailRequired | string | Customer email address (unique per store) |
firstName | string | First name |
lastName | string | Last name |
phone | string | Phone number |
notes | string | Internal notes |
address | object | Address: { line1, line2?, city, state?, country, postalCode? } |
curl -X POST "https://api.finxa.store/api/customers" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"firstName": "Ahmed",
"lastName": "Al-Khalifa",
"phone": "+97312345678",
"address": {
"line1": "Building 42, Road 3601",
"city": "Manama",
"country": "BH",
"postalCode": "0360"
}
}'Update Customer
/api/customers/:idUpdate customer fields. Only send the fields you want to change.
write_customersDelete Customer
/api/customers/:idDelete a customer permanently.
write_customerscustomerId: null.Discounts
Create discount codes (percentage or fixed amount) that customers can apply at checkout. Discounts support usage limits, date ranges, minimum order amounts, and can be targeted to specific products or collections.
List Discounts
/api/discountsRetrieve all discount codes.
read_discountsGet Discount
/api/discounts/:idRetrieve a single discount with product/collection associations.
read_discountsCreate Discount
/api/discountsCreate a new discount code.
write_discountsRequest Body
| Parameter | Type | Description |
|---|---|---|
titleRequired | string | Internal title for the discount |
codeRequired | string | Discount code customers will enter (unique per store) |
typeRequired | string | PERCENTAGE or FIXED_AMOUNT |
valueRequired | number | Discount value (e.g. 10 for 10% or 5.000 for 5 BHD) |
trigger | string | CODE (default) or AUTOMATIC |
appliesTo | string | ORDER (default), PRODUCT, or COLLECTION |
minimumOrderAmount | number | Minimum order total to apply. Default: 0 |
usageLimit | number | Max total uses (null = unlimited) |
onePerCustomer | boolean | Limit one use per customer. Default: false |
startsAt | string | ISO 8601 start date |
endsAt | string | ISO 8601 end date |
productIds | string[] | Product IDs for product-level discounts |
collectionIds | string[] | Collection IDs for collection-level discounts |
curl -X POST "https://api.finxa.store/api/discounts" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Summer Sale",
"code": "SUMMER15",
"type": "PERCENTAGE",
"value": 15,
"minimumOrderAmount": 20,
"startsAt": "2026-06-01T00:00:00Z",
"endsAt": "2026-08-31T23:59:59Z"
}'Update Discount
/api/discounts/:idUpdate a discount. Only send the fields you want to change.
write_discountsDelete Discount
/api/discounts/:idDelete a discount code.
write_discountsShipping
Manage shipping zones and rates. A shipping zone defines which countries/regions are eligible and what rates to charge.
List Shipping Zones
/api/shipping/zonesRetrieve all shipping zones with their rates.
read_shippingGet Shipping Zone
/api/shipping/zones/:idRetrieve a single shipping zone.
read_shippingCreate Shipping Zone
/api/shipping/zonesCreate a new shipping zone with rates.
write_shippingcurl -X POST "https://api.finxa.store/api/shipping/zones" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Bahrain Domestic",
"countries": ["BH"],
"rates": [
{ "name": "Standard Shipping", "price": 1.500, "minOrderAmount": 0 },
{ "name": "Free Shipping", "price": 0, "minOrderAmount": 25 }
]
}'Update Shipping Zone
/api/shipping/zones/:idUpdate a shipping zone and its rates.
write_shippingDelete Shipping Zone
/api/shipping/zones/:idDelete a shipping zone.
write_shippingPages (CMS)
Create and manage static pages for your storefront — About Us, Contact, FAQ, Shipping Policy, etc. Pages support rich HTML content.
List Pages
/api/pages/:storeIdRetrieve all pages for your store.
read_contentGet Page
/api/pages/:storeId/:pageIdRetrieve a single page.
read_contentCreate Page
/api/pages/:storeIdCreate a new page.
write_contentRequest Body
| Parameter | Type | Description |
|---|---|---|
titleRequired | string | Page title |
handle | string | URL slug (auto-generated from title if omitted) |
body | string | Rich HTML content |
published | boolean | Whether the page is published. Default: false |
seoTitle | string | SEO title |
seoDescription | string | SEO description |
curl -X POST "https://api.finxa.store/api/pages/YOUR_STORE_ID" \
-H "X-Finxa-Access-Token: fxat_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"title": "About Us",
"body": "<h2>Our Story</h2><p>We started in 2024 with a mission to...</p>",
"published": true
}'Update Page
/api/pages/:storeId/:pageIdUpdate a page.
write_contentDelete Page
/api/pages/:storeId/:pageIdDelete a page permanently.
write_contentMetafields & app storage
Attach your own data to products, orders and the store: one JSON value per (namespace, key), up to 64 KB. Use a namespace of your own so two apps never collide. Store-level metafields are your app's settings storage. Product metafield changes emit products/update, so other apps stay in sync.
/api/products/{productId}/metafields?namespace=Metafields of a product, optionally filtered by namespace.
read_products/api/products/{productId}/metafields/{namespace}/{key}Create or replace one value — 201 on create, 200 on update.
write_products/api/products/{productId}/metafieldsUpsert up to 100 values in one call.
write_products/api/products/{productId}/metafields/{namespace}/{key}Remove one value.
write_products/api/orders/{id}/metafields/{namespace}/{key}The same endpoints exist under /api/orders/{id}/metafields.
write_orders/api/metafields/{namespace}/{key}Store-level values (your app's storage): GET/POST /api/metafields, GET/PUT/DELETE /api/metafields/{namespace}/{key}.
write_store{ "value": { "code": "ERP-001", "bin": "A-12" }, "type": "json" }
// → 201 on create, 200 on update
{ "success": true, "data": { "id": "…", "namespace": "erp", "key": "sku",
"value": { "code": "ERP-001", "bin": "A-12" }, "type": "json", "app_id": "app_…",
"created_at": "2026-09-21T08:00:00.000Z", "updated_at": "2026-09-21T08:00:00.000Z" } }All endpoints of this resource in the reference →
Files & images
Product images and videos are uploaded straight to storage through a signed URL, then attached by URL. Allowed: JPEG, PNG, WebP, GIF, SVG, MP4, WebM.
/api/uploads/presignSigned upload URL for { filename, contentType } → { uploadUrl, fileUrl, key }.
write_products/api/products/{productId}/imagesAttach an uploaded (or externally hosted) image: { src, alt?, position? }.
write_products// 1. ask for a signed upload URL
const { data } = await (await api("/uploads/presign", { method: "POST",
body: JSON.stringify({ filename: "hero.png", contentType: "image/png" }) })).json();
// 2. PUT the bytes straight to storage (no token needed on this request)
await fetch(data.uploadUrl, { method: "PUT", headers: { "Content-Type": "image/png" }, body: fileBytes });
// 3. attach it to the product by URL
await api(`/products/${productId}/images`, { method: "POST",
body: JSON.stringify({ src: data.fileUrl, alt: "Hero", position: 0 }) });All endpoints of this resource in the reference →
Store settings
Read the store's settings and update the merchant-editable ones. Payment secrets and the storefront lock password never leave the API for apps.
/api/stores/meThe store: id, handle, name, currency, language, SEO, pixels, theme and more (secrets stripped).
read_store/api/stores/meUpdate settings: name, language, seoTitle, seoDescription, ga4MeasurementId, metaPixelId.
write_store{ "name": "Sunglasses BHR", "seoTitle": "Sunglasses in Bahrain", "language": "ar", "ga4MeasurementId": "G-XXXX" }All endpoints of this resource in the reference →
Inventory
Stock lives on the variant. Adjust by a delta for movements you record, or set an absolute quantity after a count; every change is logged with a reason and emits inventory_levels/update.
/api/inventory/{variantId}Current stock and the last changes for a variant.
read_products/api/inventory/{variantId}/adjustChange stock by { delta, reason? } → { before, after }.
write_products/api/inventory/{variantId}/setSet stock to { quantity, reason? }.
write_products/api/inventory/{variantId}/historyFull change history for a variant.
read_products{ "delta": -2, "reason": "Damaged in warehouse" }
// → { "success": true, "data": { "before": 5, "after": 3 } } and an inventory_levels/update webhookAll endpoints of this resource in the reference →
Markets
A market groups countries with a currency, price list and shipping zones. Prices can be set per market; product publishing can be limited per market.
/api/marketsAll markets with their countries and currency.
read_markets/api/marketsCreate a market.
write_markets/api/markets/{id}Update name, countries, currency settings.
write_markets/api/markets/{id}/pricesPer-variant prices in this market.
read_markets/api/markets/{id}/pricesSet prices for variants in this market.
write_markets/api/market-publishing/{marketId}Which products are published or excluded in a market.
read_marketsAll endpoints of this resource in the reference →
Translations
Enable locales and translate products, collections, pages, menus and theme strings. Storefront requests in a locale get the translated fields automatically.
/api/translations/localesEnabled locales and their status.
read_content/api/translations/localesEnable a locale.
write_content/api/translations/resource/{resourceType}/{resourceId}Translations of one resource (e.g. product/{id}) across locales.
read_content/api/translationsWrite translations: { resourceType, resourceId, locale, fields }.
write_content/api/translations/translate-allMachine-translate everything missing into a locale (async job; poll /translate-all/{jobId}).
write_contentAll endpoints of this resource in the reference →
Returns
Return requests move through approve → receive → process (restock and refund) or decline. Each step is its own call so warehouse and finance can act separately.
/api/returnsReturns, filterable by status.
read_orders/api/returnsOpen a return for an order's items.
write_orders/api/returns/{id}/approveApprove the request.
write_orders/api/returns/{id}/receiveMark the items as received.
write_orders/api/returns/{id}/processRestock and refund.
write_orders/api/returns/{id}/declineDecline with a reason.
write_ordersAll endpoints of this resource in the reference →
Abandoned carts
Checkouts that were started but not completed. Recovery e-mails are sent by the platform on a schedule; you can list, inspect and re-send, and tune the settings.
/api/abandoned-cartAbandoned checkouts, newest first.
read_orders/api/abandoned-cart/{id}One checkout with its items and recovery status.
read_orders/api/abandoned-cart/{id}/sendSend (or re-send) the recovery e-mail now.
write_orders/api/abandoned-cart/settingsRecovery settings: delay, discount, subject.
read_orders/api/abandoned-cart/settingsUpdate recovery settings.
write_ordersAll endpoints of this resource in the reference →
Tax
Tax-inclusive or exclusive pricing, and rates per country or region. The checkout applies the matching rate automatically.
/api/tax/settingsTax settings of the store.
read_store/api/tax/settingsUpdate tax settings.
write_store/api/tax/ratesConfigured tax rates.
read_store/api/tax/ratesAdd a rate for a country/region.
write_store/api/tax/lookup?country=The rate that applies to a destination.
read_storeAll endpoints of this resource in the reference →
Domains
Connect custom domains to the storefront. Add the domain, point DNS as instructed, then verify; one domain is primary.
/api/domainsConnected domains and their verification status.
read_store/api/domainsAdd a domain.
write_store/api/domains/{domainId}/verifyVerify DNS.
write_store/api/domains/{domainId}/set-primaryMake it the primary domain.
write_store/api/domains/instructionsThe DNS records to set.
read_storeAll endpoints of this resource in the reference →
Payment methods
Manual methods (cash on delivery, bank transfer) are fully manageable. Gateway credentials (Tap, Stripe, PayPal, Checkout.com) can be toggled and read without secrets.
/api/payment-methods/manualManual payment methods.
read_store/api/payment-methods/manualAdd a manual method: { name, instructions, isActive }.
write_store/api/payment-methods/manual/{id}Update a manual method.
write_store/api/payment-methods/tapGateway status (enabled, public key) — also /stripe, /paypal, /checkoutcom.
read_store/api/payment-methods/tap/toggleEnable or disable the gateway.
write_storeAll endpoints of this resource in the reference →
Analytics
Read-only storefront traffic: sessions, page views, top pages and who is on the site right now.
/api/analytics/{storeId}/overview?days=Sessions, views and conversion for the last N days.
read_analytics/api/analytics/{storeId}/top-pagesMost viewed pages.
read_analytics/api/analytics/{storeId}/liveVisitors active in the last few minutes.
read_analyticsAll endpoints of this resource in the reference →
Blog
Posts with rich HTML bodies, cover images, tags and scheduling. Published posts appear on the storefront blog and in the Storefront API.
/api/blog/{storeId}Posts (drafts included).
read_content/api/blog/{storeId}Create a post; set publishedAt in the future to schedule it.
write_content/api/blog/{storeId}/{id}Update a post.
write_content/api/blog/{storeId}/{id}Delete a post.
write_content{ "title": "Summer drop", "handle": "summer-drop", "body": "<p>…</p>", "excerpt": "New frames.", "tags": ["news"],
"isPublished": true, "publishedAt": "2026-09-22T09:00:00Z", "coverImage": "https://…/cover.jpg" }All endpoints of this resource in the reference →
Navigation
Menus are trees of items linking to collections, products, pages or URLs, with optional dynamic collection groups and mega-menu panels.
/api/navigation/{storeId}Menus of the store.
read_content/api/navigation/{storeId}Create a menu with its items.
write_content/api/navigation/{storeId}/{menuId}Replace a menu's title and items.
write_content/api/navigation/resolve?store=&handle=Public: a menu resolved to links (used by the storefront).
All endpoints of this resource in the reference →
Theme
Per-section overrides of the storefront theme settings (colors, layout options) without touching the theme itself.
/api/theme-overridesCurrent overrides.
read_content/api/theme-overridesSet overrides.
write_content/api/theme-overrides/mergedTheme settings with overrides applied.
read_content/api/theme-overridesRemove all overrides.
write_contentAll endpoints of this resource in the reference →
Reviews
Product reviews left by customers, with moderation (approve, hide, reply) and store-level settings.
/api/reviews/{storeId}?status=Reviews, filterable by status.
read_products/api/reviews/{storeId}/{id}Approve, hide or reply to a review.
write_products/api/reviews/{storeId}/bulkBulk moderation.
write_products/api/reviews/{storeId}/settingsReview settings (auto-approve, verified-only).
write_productsAll endpoints of this resource in the reference →
Duties
Import duty rates per destination country, used to estimate duties at checkout for international orders.
/api/dutiesConfigured duty rates.
read_markets/api/dutiesAdd a rate for a country.
write_markets/api/duties/estimate?country=&subtotal=Estimated duties for a destination and subtotal.
read_marketsAll endpoints of this resource in the reference →
Team
Team members, roles and invitations. These endpoints are for dashboard sessions; app tokens cannot manage the team.
/api/team/membersMembers and their roles.
/api/team/membersInvite a member by e-mail.
/api/team/members/{id}Change a member's role or permissions.
All endpoints of this resource in the reference →
Import, export & invoices
Bulk product data in and out as CSV, and order invoices as PDF.
/api/csv/products/exportProducts as CSV.
read_products/api/csv/products/importImport products from a CSV upload (async; returns a job id).
write_products/api/invoices/{orderId}PDF invoice for an order.
read_ordersAll endpoints of this resource in the reference →
Client libraries & tools
There is no official SDK yet — the API is plain JSON over HTTPS, so a small client is all you need. Copy one of these, or generate a typed client from the OpenAPI document.
// finxa.ts — a 30-line client: token, versioning, idempotency, pagination
const BASE = "https://api.finxa.store/api";
export class Finxa {
constructor(private token: string) {}
async request<T>(method: string, path: string, body?: unknown, idempotencyKey?: string): Promise<T> {
const res = await fetch(BASE + path, {
method,
headers: {
"X-Finxa-Access-Token": this.token,
"X-Finxa-API-Version": "2026-09",
...(body ? { "Content-Type": "application/json" } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 429) { // rate limited: wait and retry once
await new Promise((r) => setTimeout(r, Number(res.headers.get("Retry-After") ?? 1) * 1000));
return this.request(method, path, body, idempotencyKey);
}
const json = await res.json();
if (!res.ok || json.success === false) throw new Error(typeof json.error === "string" ? json.error : JSON.stringify(json.error));
return json as T;
}
get = <T>(path: string) => this.request<T>("GET", path);
post = <T>(path: string, body: unknown, key?: string) => this.request<T>("POST", path, body, key);
patch = <T>(path: string, body: unknown) => this.request<T>("PATCH", path, body);
/** Walk a page/limit list to the end. */
async *pages<T>(path: string, limit = 100) {
for (let page = 1; ; page++) {
const { data } = await this.get<{ data: T[] }>(`${path}${path.includes("?") ? "&" : "?"}page=${page}&limit=${limit}`);
yield* data;
if (data.length < limit) return;
}
}
}
const finxa = new Finxa(process.env.FINXA_TOKEN!);
for await (const p of finxa.pages<{ id: string; title: string }>("/products?status=ACTIVE")) console.log(p.title);import os, requests
BASE = "https://api.finxa.store/api"
H = {"X-Finxa-Access-Token": os.environ["FINXA_TOKEN"], "X-Finxa-API-Version": "2026-09"}
def get(path, **params):
r = requests.get(BASE + path, headers=H, params=params, timeout=30)
r.raise_for_status()
return r.json()["data"]
def post(path, body, idempotency_key=None):
h = {**H, "Content-Type": "application/json"}
if idempotency_key: h["Idempotency-Key"] = idempotency_key
r = requests.post(BASE + path, headers=h, json=body, timeout=30)
r.raise_for_status()
return r.json()["data"]
for order in get("/orders", status="PAID", limit=50):
print(order["orderNumber"], order["total"])# Import into Postman or Insomnia: File → Import → paste the URL
https://api.finxa.store/api/openapi.json
# Generate a typed client in any language (needs Java or Docker)
npx @openapitools/openapi-generator-cli generate \
-i https://api.finxa.store/api/openapi.json -g typescript-fetch -o ./finxa-client
# other generators: python, go, java, csharp, php, ruby, kotlin, swift5, dart …
# TypeScript types only, no runtime
npx openapi-typescript https://api.finxa.store/api/openapi.json -o finxa-api.d.tsScopes Reference
When creating a Custom App, you select which scopes it needs. Scopes control what resources the app can access. A write_ scope always includes the corresponding read_ scope.
| Scope | Access | Description |
|---|---|---|
read_products | Read | View products, variants, images, and inventory levels |
write_products | Read + Write | Create, update, and delete products, variants, and images |
read_collections | Read | View collections and their products |
write_collections | Read + Write | Create, update, and delete collections |
read_orders | Read | View orders, line items, and fulfillment status |
write_orders | Read + Write | Update order status, add tracking, manage fulfillment |
read_customers | Read | View customer profiles and addresses |
write_customers | Read + Write | Create, update, and delete customers |
read_discounts | Read | View discount codes and rules |
write_discounts | Read + Write | Create, update, and delete discounts |
read_shipping | Read | View shipping zones, rates, and rules |
write_shipping | Read + Write | Create, update, and delete shipping zones and rates |
read_content | Read | View pages and CMS content |
write_content | Read + Write | Create, update, and delete pages |
read_store | Read | View store configuration, tax rates, and payment methods |
write_store | Read + Write | Update store settings, tax rates, and payment methods |
read_markets | Read | View markets, price lists, and localization settings |
write_markets | Read + Write | Create, update, and delete markets and price lists |
read_analytics | Read | View storefront sessions, page views, and traffic data |
read_webhooks | Read | View webhook subscriptions |
write_webhooks | Read + Write | Create, update, and delete webhook subscriptions |
API reference
Every endpoint of the live API, generated from the OpenAPI document the API builds from its own route table — it cannot list a route that does not exist, and new routes appear here on deploy. The guides above cover the main resources; use search (⌘K) to jump to any operation.
https://api.finxa.store/api/openapi.jsonLoading the live endpoint list…
Quick Start Guide
Get up and running in 5 minutes. This example creates a product, then lists all products using Node.js.
const API_URL = "https://api.finxa.store/api";
const TOKEN = "fxat_your_secret_key_here";
const headers = {
"X-Finxa-Access-Token": TOKEN,
"Content-Type": "application/json",
};
// 1. Create a product
async function createProduct() {
const res = await fetch(`${API_URL}/products`, {
method: "POST",
headers,
body: JSON.stringify({
title: "My First Product",
status: "ACTIVE",
variants: [
{ title: "Default", price: 19.99, inventory: 50 }
],
}),
});
const data = await res.json();
console.log("Created:", data.data.id);
return data.data;
}
// 2. List all products
async function listProducts() {
const res = await fetch(`${API_URL}/products`, { headers });
const data = await res.json();
console.log(`Found ${data.data.pagination.total} products`);
return data.data.products;
}
// 3. Update a product
async function updateProduct(productId) {
const res = await fetch(`${API_URL}/products/${productId}`, {
method: "PATCH",
headers,
body: JSON.stringify({
description: "Updated via the API!",
tags: ["api-managed"],
}),
});
const data = await res.json();
console.log("Updated:", data.data.title);
}
// Run it
const product = await createProduct();
await updateProduct(product.id);
await listProducts();import requests
API_URL = "https://api.finxa.store/api"
HEADERS = {
"X-Finxa-Access-Token": "fxat_your_secret_key_here",
"Content-Type": "application/json",
}
# Create a product
res = requests.post(f"{API_URL}/products", headers=HEADERS, json={
"title": "My First Product",
"status": "ACTIVE",
"variants": [
{"title": "Default", "price": 19.99, "inventory": 50}
],
})
product = res.json()["data"]
print(f"Created: {product['id']}")
# List all products
res = requests.get(f"{API_URL}/products", headers=HEADERS)
data = res.json()["data"]
print(f"Found {data['pagination']['total']} products")
# Update a product
res = requests.patch(f"{API_URL}/products/{product['id']}", headers=HEADERS, json={
"description": "Updated via the API!",
"tags": ["api-managed"],
})
print(f"Updated: {res.json()['data']['title']}")Build powerful integrations with the Finxa Commerce API.