Headdy
A token-authenticated headless storefront API for Craft Commerce.
Craft Commerce has exposed products through GraphQL since version 2. It has never exposed a single
cart or order mutation — [craftcms/commerce discussion #2350][2350] has been open for years, and
the standing advice is to give up on GraphQL for the cart and POST form-encoded requests to
actions/commerce/cart/update-cart with a PHP session cookie and a CSRF token.
That works for a Twig site posting to itself. For a Next, Nuxt or Astro front end on another origin
it means credentials: 'include', a SameSite policy that survives your CDN, a round trip to fetch
a CSRF token, responses shaped like Craft's internal element attributes, and a payment step that is
simply unreachable because Craft hashes the return URL into a Twig form your JavaScript cannot
produce.
WooCommerce has the Store API and CoCart. Craft has had nothing. Headdy is that layer.
# One request: create a cart with an item in it.
curl -X POST https://example.com/api/storefront/v1/carts \
-H 'Content-Type: application/json' \
-H 'X-Headdy-Key: hd_pk_…' \
-d '{"items":[{"purchasableId":123,"qty":2}]}'
{
"success": true,
"cart": {
"token": "hdc_…",
"totalQty": 2,
"lineItems": [{ "id": 456, "sku": "TSHIRT-L", "qty": 2, "total": { "amount": "49.98", "minorUnits": 4998, "currency": "USD", "formatted": "$49.98" } }],
"totals": { "total": { "amount": "49.98", "minorUnits": 4998, "currency": "USD", "formatted": "$49.98" } }
}
}
No cookie. No CSRF token. Hold cart.token, send it back as X-Headdy-Cart, and you have a cart.
What you get
- A REST storefront API — carts, line items, addresses, coupons, shipping methods, checkout,
payment, catalog, customer accounts. Versioned at
/v1, with a documented JSON contract and stable machine-readable error codes. - GraphQL cart mutations —
headdyCartCreate,headdyCartAddItem,headdyCartApplyCouponand the rest, registered on Craft's own GraphQL endpoint. (Pro) - Payment that actually works headlessly — including off-site gateways, whose redirect comes back as JSON for your front end to act on rather than as a 302 it cannot follow.
- Bearer tokens instead of sessions — issued once, stored only as a SHA-256 hash, revocable, and rotated on checkout.
- Money you can trust — every amount is
{amount, minorUnits, currency, formatted}, never a bare float. - A configuration check that finds the misconfiguration before your front-end team does.
The one thing worth knowing about the design
services\Carts::update() is the only place a cart changes. Every REST endpoint, every GraphQL
mutation and every console command builds a parameter array and hands it to that method. The
granular verbs (POST /carts/current/items, PATCH …/items/{id}) exist because they read well —
each is a few lines that assembles params and calls update().
So REST and GraphQL cannot drift apart, and there is exactly one place where stock checks, option signatures, recalculation order and validation happen.
Mutations are also applied in a fixed order regardless of the order you wrote them: clear → items → addresses → email → coupon → shipping → gateway → fields. Setting a shipping method before an address exists would pick from an empty option list; applying a coupon before the items are in would not meet the discount's minimum. One big patch and six small ones give the same answer.
To install this plugin, copy the command above to your terminal.
This plugin doesn't have any reviews.






