JustFlows

Shop · Developers

Build on Shop

Run a headless storefront, sync orders to your back office, or extend Shop from your own plugin. Everything here is the same surface the Shop admin and checkout use.

What you can build

Headless storefronts

Read the catalog, run the cart and checkout, and show customer accounts from any frontend with the public and customer operations.

Back-office integrations

Sync orders, customers, and subscriptions to an ERP or accounting tool with cursor lists, and react to typed events through signed webhooks.

Plugins that extend Shop

Add payment gateways, carriers, tax providers, promotions, and product types through filters, typed by justflows.shop/contracts.

AI agents

71 shop_* MCP tools run the same staff routes as the admin, with the same checks.

Base URL

text
https://your-site.example/api/shop/v1

The older /ext/justflows.shop/v1 address is an alias. The API exists only on sites where Shop is active. Paths in this reference are relative to the base URL.

Authentication

Shop uses the host’s site-bound session, not API keys. Every operation is one of three kinds:

AccessWhoHow
publicAnyoneStorefront, cart, and checkout. No sign-in. Writes send the CSRF token.
customerA signed-in customerThe jf_session cookie. Shop checks that the order, link, or subscription belongs to the account.
staffStaff with a Shop capabilityThe jf_session cookie and one of the capabilities listed on each operation, or shop:settings.

Every write keeps the host’s CSRF policy. A guest first calls GET /api/auth/csrf, keeps the jf_csrf cookie, and sends its value as x-csrf-token. Cart and checkout calls send the cart’s token as x-jf-cart; keep it private. Responses are Cache-Control: private, no-store.

A headless checkout

Guest checkout from another frontend
const SHOP = "https://your-site.example/api/shop/v1";

// 1. A guest asks for the jf_csrf cookie and sends its value back on every write.
await fetch("https://your-site.example/api/auth/csrf", { credentials: "include" });
const csrf = document.cookie.match(/(?:^|; )jf_csrf=([^;]+)/)?.[1];

async function shop(path, { method = "GET", body, cart } = {}) {
  const headers = { "Content-Type": "application/json" };
  if (method !== "GET") headers["x-csrf-token"] = csrf;
  if (method !== "GET" && path !== "checkout/quote") headers["Idempotency-Key"] = crypto.randomUUID();
  if (cart) headers["x-jf-cart"] = cart;
  const res = await fetch(`${SHOP}/${path}`, { method, headers, credentials: "include", body: body && JSON.stringify(body) });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error.message), json.error);
  return json.data;
}

// 2. Add a product. The first add starts a cart and returns its token: keep it private.
const { token: cart } = await shop("cart/items", { method: "POST", body: { contentId: productId, quantity: 1 } });

// 3. Price the order without placing it.
const order = { email: "sam@example.com", shipping: address, billingSame: true, methodId, paymentId, acceptTerms: true };
const quote = await shop("checkout/quote", { method: "POST", body: order, cart });

// 4. Place it and send the shopper to the payment page.
const placed = await shop("checkout", { method: "POST", body: order, cart });
window.location.href = placed.redirectUrl;

Responses and errors

A successful operation wraps its result in data. Failures use one envelope, and field names the input to fix when it is known: a body field such as email, or an address field such as shipping.postal.

200
{ "data": { "ok": true, "orderNumber": "JF-261027-K4P9QZ" } }
400
{
  "error": {
    "code": "address_invalid",
    "message": "Shipping address needs a street, city, postal code, and country.",
    "field": "shipping.postal"
  }
}
StatusCodeMeaning
400invalid_request, idempotency_key_required, client_identity_required, invalid_limit, invalid_cursorThe request is incomplete or invalid.
401unauthorizedSign in first.
403forbiddenThe session lacks the capability.
404not_foundNo such resource on this site, or not yours.
409setup_requiredShop setup is not finished, or Shop needs reactivating after an upgrade.
409idempotency_conflictThe key was already used with a different request.
409idempotency_in_progressAn earlier request with this key has no confirmed outcome.
413request_too_largeThe request is over 1 MB.
429rate_limitedToo many requests: 60 a minute for the v1 operations.
502label_unknownThe carrier did not say whether it made the label.

Idempotency keys

Every v1 mutation needs an Idempotency-Key of 8–128 printable ASCII characters. checkout/quote is read-only and needs none. Generate a fresh UUID for each logical operation and keep it while you retry.

  • Repeating the same request replays the saved response with Idempotency-Replayed: true.
  • The same key with a different body, query, parameters, or locale returns 409 idempotency_conflict.
  • A request interrupted after Shop claimed the key returns 409 idempotency_in_progress. Inspect the order or payment before using a new key, so an interrupted provider call never charges twice.
  • Keys are scoped to the site, the session or cart, the method, and the route, and are kept until Shop data is deleted. Only a salted fingerprint of the request is stored.

Lists and pagination

GET orders, GET customers, and GET subscriptions return site-scoped pages sorted by created_at, then id. Pass limit (1–100, default 50) and the opaque cursor from the previous page, and stop when nextCursor is null. A cursor belongs to one resource, and edits can show between pages. Lists expose documented scalar fields only, never payment references or private order keys.

json
{ "data": [], "page": { "limit": 50, "nextCursor": null } }

Money

Amounts you send are decimal strings in major units, such as "10.00", never JSON numbers. Stored totals and event amounts are integer minor units with a currency and precision: 2541 EUR at precision 2 is EUR 25.41.

OpenAPI and contracts

GET /api/shop/v1/openapi.json describes the surface in OpenAPI 3.1, and the same operations are merged into the host’s /api/v1/openapi.json through the openapi.document filter. For TypeScript, the justflows.shop/contracts export provides ShopApiError, ShopApiPage<T>, ShopEventMap, PaymentGateway, ShippingCarrier, ShopPaymentAdapter, and ShopCarrierAdapter. Type imports never load Shop’s runtime.

Versioning

The HTTP version and each event’s data.version are independent. Adding an optional field is compatible. Removing a field, changing its meaning or unit, or making a field required needs a new major version and a changelog entry, and v1 stays available while clients move.