Operator
What it runs The suite, in detail

Two lines of HTML, on your own page.

The checkout is a custom element with a shadow root. Your stylesheet cannot reach in and break it, and it cannot reach out and restyle your site. No iframe, no redirect to somebody else's brand, and no script that takes over the page it was dropped onto.

Custom element · shadow DOM · no framework required on your side

Putting it on the page

A script tag and an element. The channel is the public identifier of one sale; without it nothing renders.

<script type="module" src="https://cdn.hibilet.com/hi-pos.js"></script>

<hi-pos channel="0193f2…" api="https://api.yourdomain.com"></hi-pos>

That is the whole integration. There is no SDK to install, no build step on your side, and nothing to initialise in JavaScript. Drop it into WordPress, Webflow, a static page or a React app and it behaves the same way, because it is a browser primitive rather than a framework component.

Nothing on this page needs a merchant token. The element only ever calls the public channel endpoints. It never sees a secret, it never computes a price it then sends back, and it never learns anything about your account beyond the branding it is told to wear.

Or don't embed it at all

If you have nowhere to put it, the same component is a page: store.html?channel=… is a complete shop you can link to from a poster or a bio. Same build, same component, same behaviour on a phone and on a desktop.

Attributes

Attributes are the entire public API. Presence-based booleans are on when present and off when absent.

AttributeDefaultWhat it does
channelThe public link to one sale. Required; without it nothing renders.
api/apiAPI origin. The default suits a same-origin proxy.
layoutlistlist or grid.
densitycomfortablecomfortable or compact.
themeautolight, dark or auto.
localebrowserSent to the API, which returns translated text.
hide-stockoffHides remaining-stock counts.
hide-quantityoffHides the quantity stepper.
hide-checkoutoffCatalogue only, no path to pay.
hide-introoffDrops the name, blurb and venue from the widget's own header, for a host page that already shows them.
hide-selleroffDrops the footer naming the seller. Only for a page that names them itself — somebody has to.

Theming it

Colours are deliberately not attributes. They are CSS custom properties, which inherit through the shadow boundary, so you theme the checkout with an ordinary stylesheet instead of stringifying hex codes into HTML.

hi-pos {
  --pos-accent: #0f766e;
  --pos-radius: 12px;
}

Your account's own theme is applied as a :host rule inside the shadow root, specifically so that a rule you write on the host page still wins. Our branding is a default, never an override.

Why it fits anywhere

Every size decision is a container query, not a media query. The element declares container-type: inline-size, so it measures the space it was given rather than the browser window.

Container widthWhat it becomes
under ~48remOne column, sticky action bar, basket as a sheet
~48rem and upTwo columns, basket rail always visible, no action bar
~56rem and upProducts in two columns as well

This is the difference between an embed that works and one that does not. A media query asks the window, so the same widget dropped into a 380px sidebar on a 27-inch monitor would lay itself out as a desktop shop and overflow the space it was given. Asking the container means the embed and the standalone page are one component answering the same question correctly in both places.

It is designed at 360px and allowed to grow, not shrunk down from a desktop layout. The assumption is a phone, in a queue, on mobile data, one-handed. Two steps, each one screen: catalogue, then checkout, then done. The basket and the extras are sheets over the catalogue, never screens of their own.

The payment handshake

A sale whose merchant has connected a payment provider sends the shopper to a payment page on the merchant's own account rather than settling inside the widget. Three things are worth knowing about how that is handled, because each one is a bug in most integrations.

  • The basket is not completed by leaving. Stock stays held and the hold is extended to cover the payment window. The order exists when the provider's webhook reaches the API, not when the browser comes back.
  • Coming back is not proof of payment. ?paid=1 is a query parameter anybody can type. The widget polls the payment session and waits for settled.
  • The shopper who pays and closes the tab still gets their ticket. Nothing depended on their browser returning, and their details were remembered before they left for exactly that reason.

Four outcomes, and they read differently on purpose. Settled shows the order code. Cancelled and expired leave the basket alone, because walking away from a till is not a failure. Failed means paid-but-unfulfillable, and is already refunded. Running out of patience says the payment went through and the confirmation is still coming.

Whether the card path is offered at all is decided by takes_card on the channel, which means can take a card right now: a connected account the provider is currently willing to charge on. A merchant halfway through onboarding gets the direct path instead of a button that fails at the till.

What it talks to

Only the channel endpoints, all unauthenticated, all public by design.

GET  /channels/{id}                  the sale, its products, and what to ask
POST /channels/{id}/baskets          the guest account and the basket, together
POST /v1/reservations                with the basket token
GET  /v1/baskets/{id}/agreement      the terms this sale is sold under
POST /v1/baskets/{id}/checkout       the transaction and the order code
GET  /v1/my/orders                   this session's order history
GET  /orders/{code}                  one order, no token — the way back
GET  /p/{merchant}/bundles/{slug}    the bundle page, and only that page

The server decides prices. A client may propose a quantity; it may never propose an amount.

Things that bit us

Written down because they will bite anyone building a shadow-DOM embed, not just us.

  • @property declarations do not work inside a shadow root. They register on the document or they do nothing at all, so the element hoists them out on connect.
  • The embed build must define process.env.NODE_ENV. Without it process is undefined in the browser and React's development build ships alongside the production one. Adding the define took the bundle from 630 kB to 241 kB.
  • Container queries, not viewport queries. Covered above, and it is the one that produces the most confusing bug reports: the widget is "broken on desktop" only when it is in a narrow column.

Stuck on something not covered here? info@hibilet.com reaches somebody who has read the source.