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.
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

  

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, 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
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