Headless BFF API

POST https://poc.way4tech.com/graphql
One endpoint. Every call is POST to /graphql with header Content-Type: application/json and a body { "query": "...", "variables": { ... } }. The Run buttons below hit the live staging Odoo - edit the body and re-run. Auth calls need Authorization: Bearer <token> - paste a token here after logging in:
Test customer: buyer@example.com / demo1234.
curl -X POST https://poc.way4tech.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ products { variant_id sku availability price_chf } }"}'

Products

products query

Catalog with live stock from Odoo. No parameters.

  

product query

One product with its variants.
ParamTypeNotes
name requiredStringproduct template name

  

Products (Phase 2)

productCard query

Card shape: what the list, hover quick view and drawer preview need. Small, sparse. Has has_variants for the smart "+".
ParamTypeNotes
slug requiredStringe.g. hayabusa-t3-boxing-gloves
localeStringde / fr / it / en (default de)

  

productDetail query

Full PDP body: attribute_axes with display_type + html_color per value, all variants, precomputed variant_matrix (combo_key sorted alphabetically by attribute_slug), images, badges. Each variant carries price_tiers, the volume prices from the shop pricelist, tier 1 being the unit price; the basket applies the same tiers by quantity.
ParamTypeNotes
slug requiredStringsame as productCard
localeStringde / fr / it / en

  

productList query

Cursor-paginated list of cards. next_cursor is opaque; pass it back as cursor for the next page; null on the last page.
ParamTypeNotes
category_slugStringoptional filter
localeStringde / fr / it / en
limitIntdefault 24
cursorStringfrom a previous response's next_cursor

  

Images (Phase 2b)

image pipeline how it works

One binary is stored once. The BFF mints one public URL per locale, each ending in the translated product name, so a FR crawler sees gants-de-boxe.webp and a DE crawler sees boxhandschuhe.webp for the same stored file. imgproxy only ever receives a key, a width and a format, never a language.

URL shape: /img/{base64url(s3_key)}/{width}x{dpr}/{localized-name}.{format}
Rendered pixels are width * dpr. The trailing name is decorative and exists for SEO: the key is authoritative, so renaming a product never breaks an indexed URL. Formats: webp, avif, jpg, png. Responses carry cache-control: immutable.
Field on ImageTypeNotes
srcStringresolved for the requested locale
src_i18nI18none URL per locale, same binary
srcset[ImageRendition]1x / 2x / 3x, build url 1x, url 2x, url 3x
alt / alt_i18nString / I18nlocalized alt text
width / heightIntintrinsic pixels, reserve the box so CLS stays 0
is_primaryBooleanuse this one for og:image
variant_idIntnull = template level, else the variant it belongs to
placeholder_urlStringshow while the rendition loads
s3_keyStringneutral key, no language in it
512 @1x, DE filename, webp
512 @2x, FR filename, webp
512 @3x, IT filename, webp
256 @1x, EN filename, avif

All four are the same stored binary. Only the transform and the filename differ.

productDetail images query

The image array on a product, with the localized URLs and the retina ladder. Needs Odoo up.

  

Cart

addToCart mutation

Adds a variant. First add (no cart_token) starts a guest cart = a draft sale.order in Odoo. Blocked if over stock.
ParamTypeNotes
variant_id requiredIntfrom products
qtyFloatdefault 1
cart_tokenStringomit to start a new cart; pass to add to an existing one

  

cart query

Read a cart by its token.
ParamTypeNotes
cart_token requiredStringfrom addToCart

  

Checkout (Phase 4)

checkout flow how it works

One page checkout in three steps, then a hosted payment page. Totals always come from Odoo, including Swiss VAT.

1. setCheckoutAddresses with billing and optionally a separate delivery address. A guest becomes a customer record at this point.
2. shippingMethods lists the carriers that apply to this cart with the cost each adds, then setShippingMethod picks one.
3. startPayment returns a redirect_url. Send the customer there. Payrexx brings them back to the return url (the return_url you passed, or the configured default) with ?cart=<token>&outcome=success|failed|cancel.
4. paymentStatus reads the real state from Payrexx and confirms the Odoo order once. Poll it on the return page. The Payrexx webhook calls the same logic, so the order is confirmed even if the customer closes the tab.

checkout can be called at any time. missing tells the UI what is still needed, ready_for_payment gates the pay button. Staging runs in mock mode: redirect_url opens a stand-in page with Pay, Decline and Cancel buttons instead of Payrexx.

checkout query

Current state of the checkout for a cart. Use the cart_token from addToCart.

  

setCheckoutAddresses mutation

Billing is required and needs an email, the confirmation goes there. Shipping is optional, leave it out to deliver to the billing address.
ParamTypeNotes
cart_tokenStringfrom addToCart
billingAddressInputname, email, street, zip, city, country_code, optional phone and street2
shippingAddressInputoptional separate delivery address

  

shippingMethods query

Carriers that apply to this cart and country, with the cost each would add. Free-over thresholds and margins are already applied. Rule based carriers that match no rule are left out.

  

setShippingMethod mutation

Adds the delivery line to the order in Odoo. Calling it again replaces the previous choice.

  

startPayment mutation

Creates the Payrexx gateway for the order total and returns where to send the customer. Fails with CHECKOUT_NOT_READY if anything is still missing. return_url is optional: pass the page for the customer's locale and Payrexx brings them back there; leave it out for the configured default. Only listed origins are accepted. Open redirect_url in a new tab to pay on staging.

  

paymentStatus query

Reads the gateway state from Payrexx and confirms the Odoo order on the first successful call. Safe to poll. paid is true for confirmed, authorized and reserved.

  

Auth

login mutation

Portal (res.users) authentication. Returns a stateless JWT. Pass a guest cart token to merge it into the customer on login.
ParamTypeNotes
email requiredStringportal login
password requiredStringwrong password → AUTH_INVALID_CREDENTIALS
guest_cart_tokenStringoptional - merges that cart

  

checkEmail query

Fluid Auth step 1 - enter email first. If it exists, show the password field (Login); if new, expand Registration.
ParamTypeNotes
email requiredStringreturns { email, exists } - one fast Odoo lookup

  

register mutation

Fluid Auth - new email path: creates the portal customer and logs them straight in (returns a Session, same shape as login).
ParamTypeNotes
email requiredStringthe new login
password requiredString
name requiredStringdisplay name
guest_cart_tokenStringoptional - merges the guest cart into the new account

  

mergeGuestCart mutation needs token

Explicit guest → customer merge. Requires the JWT in the box at the top (auto-filled after Login).
ParamTypeNotes
guest_cart_token requiredStringthe guest cart to fold in

  

Types

TypeFields
Variantvariant_id Int, sku String, name String, price_chf Float, free_qty Float, availability String (in_stock / low_stock / out_of_stock)
Cartcart_token String, order_id Int, partner_id Int, state String (guest / active / merged / converted), currency String, lines [CartLine], subtotal_chf / tax_chf / total_chf Float
CartLineline_id Int, variant_id Int, name String, qty Float, unit_price_chf Float (tier applied), discount_pct Float, subtotal_chf Float
Sessiontoken String (JWT), partner_id Int, display_name String, cart Cart

Errors

Failures return { "errors": [...] } with extensions.code from a fixed set and a translated extensions.user_message { de, fr, it, en }. Switch on code, never on the text.
codewhen
CART_STOCK_INSUFFICIENTordering more than available stock
CART_VARIANT_UNAVAILABLEvariant archived / inactive
AUTH_INVALID_CREDENTIALSwrong email or password
CART_NOT_FOUNDno open cart for that token
CHECKOUT_INVALID_ADDRESSunknown country code or missing address field
CHECKOUT_SHIPPING_UNAVAILABLEthe carrier does not apply to this cart or country
CHECKOUT_NOT_READYstartPayment before addresses and shipping are set, see missing
CHECKOUT_INVALID_RETURN_URLreturn_url is not http(s) or its origin is not on the allow list
PAYMENT_FAILEDPayrexx did not accept the gateway request. retryable: true
ODOO_ACCESS_DENIEDthe integration user lacks rights on a model
ODOO_UNAVAILABLEOdoo is down or unreachable. retryable: true, so show a retry rather than a hard error
INTERNALanything unmapped