Skip to main content

Storefront API

The Storefront API lets a partner-built storefront act for a signed-in buyer of one ARC seller. Your server calls it. It is server-to-server only: ARC's CORS list does not include storefront origins, so a browser cannot call it directly.

The first version is 2027-01.

POST https://<tenant host>/api/storefront/2027-01/graphql

Before you start​

  • Your app is installed in the seller's organization with the read_storefront scope. That installation gives you a storefront access token. Only a seller user who may install apps (the settings:apps:install permission) can grant read_storefront.
  • The seller's plan has the Storefront API switched on (the storefrontApi plan feature). It is off by default; ARC turns it on per organization.
  • The organization comes only from your token. The host you call, Host and X-Original-Host play no part. A token for one seller never reads another seller's data.

Two credentials on every buyer request​

Every query and mutation acts for one signed-in buyer, so each request carries two tokens:

HeaderHoldsLifetime
Authorization: Bearer <storefront access token>Your installationUntil the installation is uninstalled, paused or the token is revoked
X-Storefront-Buyer-Token: <buyer access token>The buyer1 hour; refresh it

A request without a valid buyer token is refused with 401. A buyer token works only with the installation that signed the buyer in.

Sign a buyer in​

The buyer endpoints take your storefront access token in Authorization: Bearer and nothing else.

EndpointBodyReturns
POST /api/storefront/auth/sign-in-code{ "identifier": "<buyer email or phone>" }Sends the buyer a one-time code
POST /api/storefront/auth/sign-in{ "identifier": "…", "code": "123456" }{ accessToken, refreshToken, tokenType, expiresIn, buyer }
POST /api/storefront/auth/refresh{ "refreshToken": "…" }A new token pair
POST /api/storefront/auth/sign-out{ "refreshToken": "…" }{ signedOut }

Keep both buyer tokens on your server, in the buyer's session. An access token can stay valid for up to one hour after sign-out.

What 2027-01 exposes​

The schema is a subset of the buyer schema. The exact schema for the version is published as docs/api/storefront/2027-01.graphql. Introspection is not available on this path; use the published file.

AreaQueriesMutations
Catalog and stockcustomerProducts, customerProductFacets, customerProductDetail, customerProductBySku, customerCategories, customerCategoryChildren, customerBrands—
Buyer pricingcustomerMyVariantPrice—
CartcustomerCart, customerCartSummary, customerCartBilladdToCart, updateCartItem, removeCartItem, clearCart, applyDiscountCode
Order historycustomerOrders, customerOrderDetail—
QuotescustomerQuotes, customerQuoteDetailcustomerRequestQuoteFromCart
AddressescustomerAddresses—
Checkout—placeOrderFromCart

See Checkout for what placeOrderFromCart accepts.

Anything else is refused with 403 and extensions.code: "FORBIDDEN", before any data is read. That includes every console operation, every other buyer operation and __schema / __type.

No root takes an organization, partner or customer ID. The buyer is always the one in X-Storefront-Buyer-Token.

Money​

Money is never a JSON number. Every amount comes back as an AppMoney object:

type AppMoney {
amount: String! # a decimal string, e.g. "1234.50"
currencyCode: String! # ISO 4217, e.g. "INR"
}

Each money value has a <field>Money field beside the older number field. Select the …Money field. The number field is refused on this path.

query Bill {
customerCartBill {
itemCount
subtotalMoney { amount currencyCode }
taxAmountMoney { amount currencyCode }
deliveryFeeMoney { amount currencyCode }
grandTotalMoney { amount currencyCode }
lines {
variantId
quantity
unitPriceMoney { amount currencyCode }
lineTotalMoney { amount currencyCode }
}
}
}

Where prices come from:

  • A variant's price for this buyer: customerMyVariantPrice → finalPriceMoney, listUnitPriceMoney. These come from ARC's pricing engine for that buyer and quantity. A null means there is no price in the requested currency.
  • The cart: customerCartBill re-prices every line on each read. Show this bill, not your own sum.
  • Orders and quotes: the amounts stored on the document, in the document's currency.

Catalog listing prices (CustomerProduct.price and similar) are not served on 2027-01. Ask customerMyVariantPrice or the cart bill for the buyer's price.

If the seller has not set a base currency, money fields fail with CURRENCY_NOT_CONFIGURED rather than guessing one.

Quantities, percentages, tax rates and conversion factors stay numbers.

Checkout​

placeOrderFromCart places an order from the signed-in buyer's cart. It is the same checkout the customer portal uses: ARC re-prices every line, applies tax and the delivery charge, and runs the stock, credit-limit and approval rules before it creates the order. You never send a price or a total.

mutation Checkout($input: PlaceOrderFromCartInput!) {
placeOrderFromCart(input: $input) {
orderId
orderNumber
status
totalAmountMoney { amount currencyCode }
approvalRequest {
requestNumber
status
totalAmountMoney { amount currencyCode }
}
}
}

The input fields you may send:

FieldNotes
addressIdRequired. The delivery address. It must be an active address of the signed-in buyer's account. Any other address is refused with ADDRESS_NOT_FOUND. Read the buyer's address ids from customerAddresses.
idempotencyKeyRecommended. Send a new key per checkout attempt. A retry with the same key returns the order already placed, not a second order.
expectedRevisionThe cart revision you showed the buyer. If the cart changed since, checkout is refused with CART_REVISION_CONFLICT.
notes, reference, poNumberFree text that is stored on the order.

paymentMethod, deliveryOptionId and customFields are refused on 2027-01 with 403. Every storefront order is placed on account, on the buyer's own payment terms, exactly as when a buyer chooses "On account" in the customer portal. So ARC first checks that the buyer may pay on account: if the seller does not offer the buyer's payment term as a credit term, checkout is refused with ON_ACCOUNT_UNAVAILABLE and no order is created. The credit limit then applies as usual. Online payment is not available on this version.

What can come back:

  • An order: orderId, orderNumber, status and totalAmountMoney are set. status is usually CONFIRMED.
  • An approval request: when the seller requires approval for this order, no order is created yet. orderId is null and approvalRequest is set. The cart stays as it was.
  • A refusal: a GraphQL error, usually with a machine code in extensions.code, for example ADDRESS_NOT_FOUND, CART_REVISION_CONFLICT, ON_ACCOUNT_UNAVAILABLE or OVER_CREDIT_LIMIT. A stock shortfall is refused with a message naming the item. Do not retry a refusal with the same input.

On success the cart is emptied.

Pick the delivery address​

customerAddresses lists the addresses the signed-in buyer may order to. It takes no argument. A buyer whose account owner assigned them some addresses sees only those.

query Addresses {
customerAddresses { id label type isDefault isDualPurpose }
}

id, label, type, isDefault and isDualPurpose are always available, so you can let the buyer pick an address without holding contact data. The street, city, state, postal code, country, contact name and phone, delivery notes and map position need read_customer_pii (see below). The list is read-only on 2027-01.

Contact data and custom fields​

  • Addresses on orders (shippingAddress, billingAddress) are contact data. They need the read_customer_pii scope on your installation; without it the field is refused.
  • On customerAddresses, every location and contact field needs read_customer_pii too. Only id, label, type, isDefault and isDualPurpose are served without it.
  • customFields values are not served on 2027-01, and the cart, quote and checkout mutations do not accept them.

Limits​

Limits fail closed: if ARC cannot check them, the request is refused.

LimitValueRefusal
Cost of one operation1000 points400, MAX_COST_EXCEEDED
Your installation's storefront bucket1000 points, refills 50 per second429, THROTTLED, with retryAfterSeconds
Limiter unavailable—503, RATE_LIMITER_UNAVAILABLE

Every response that reached the limiter carries extensions.cost:

{
"extensions": {
"cost": {
"requestedQueryCost": 12,
"throttleStatus": { "maximumAvailable": 1000, "currentlyAvailable": 988, "restoreRate": 50 }
}
}
}

A scalar costs 0, an object 1, and a list counts its page size (or 100 rows at the root and 10 rows nested when it has none). The storefront bucket is separate from your app API bucket.

Versions​

Storefront versions follow the app API's calendar: a version is supported for at least 12 months after it ships. A deprecated version still answers, with Deprecation and Sunset headers. A sunset version answers 410. A field is never removed from a supported version; a breaking change ships in a new version.

Errors​

Statusextensions.codeMeaning
401UNAUTHENTICATEDThe storefront token or the buyer token is missing, expired or revoked
403FORBIDDENThe operation, field or input is not in this version, or needs a scope you do not hold
400MAX_COST_EXCEEDEDThe operation costs more than 1000 points
429THROTTLEDYour storefront bucket is full; retry after retryAfterSeconds
503RATE_LIMITER_UNAVAILABLE / SERVICE_UNAVAILABLEARC could not check the request; retry shortly