UI Extensibility - How to develop UI Extensions

UI Extensibility - How to develop UI Extensions

Read our introduction to UI Extensibility and how to configure an UI extension before diving into this article.

1. Introduction

OneStock UI Extensions let you plug custom web applications into specific places (“anchors”) of the OneStock backoffice and store application.

  • A new page in the back office

  • A cta for an order detail that will open a popup

  • A cta for a bulk action in the order list that will open a popup

  • A new page in the store application

1.1. Key concepts: Extension vs Anchor

  • An extension is:

    • Identified by a single extension_id

    • Secured by a secret hash

    • Owned and hosted by you (public HTTPS URL)

  • An anchor is:

    • A location in the UI where OneStock can display your extension

    • Examples : bo.page, bo.order.action, bo.orders.action

1.2. Add an extension to your environment

To add an extension to your environment:

  1. Define the extension itself
    Provide to your OneStock contact:

    • A name for the extension

    • A description

    • A public HTTPS URL where your extension is hosted

    • An icon (png, jpg, gif up to 100KB

  2. Configure one or more anchors for this extension
    For each injection point you want to use:

    • A name : that name that will be displayed in the anchor

    • A slug : used to construct the url in the backoffice for pages and also to translate your name in different languages in the backoffice

    • Choose the anchor type, e.g.:

      • bo.page

      • bo.order.action

      • bo.orders.action

    • Choose roles who can access this extension. You can either allow only for roles or disable for roles.

      • Client admin

      • Headquarters

      • Customer service

      • Configuration Manager

    • An icon (png, jpg, gif up to 100KB)

    • The path to load for this anchor : that would be the relative path from your domain to access this anchor actions. Ex: /bulk_edit_orders

    All these anchors will share the same extension_id and secret, which means:

    • You verify signatures in the same way regardless of anchor

    • You can reuse the same codebase (single UI project) behind multiple entry URLs and anchors

  3. OneStock will:

    • Create the extension entity

    • Generate and share a secret hash (for the signature)

    • Provide the extension_id


2. Extension contract

This section is the formal contract between OneStock and your extension.

The contract has three parts:

  1. URL parameters (sent in the iframe URL)

  2. Handshake via postMessage (extension ⇄ OneStock)

  3. Security: extension signature verification

sequence.jpg

Ressource :

2.1. URL parameters

When OneStock opens your extension, it calls your public URL with query parameters:

Parameter

Type

Description

Parameter

Type

Description

extension_id

string

Unique id of the configured extension instance

user_id

string

Id of the logged OneStock user

site_id

string

Id of the OneStock account / tenant

lang

string

User language code set in the OneStock UI (e.g. en, fr)

timezone

string

User timezone (e.g. Europe/Paris)

locale

string

User locale (e.g. fr-FR), it is the value set for the date format in the user configuration.

parent_url

string

URL of the OneStock UI that embeds your extension iframe

Your extension should:

  • Parse these query parameters on load.

  • Use parent_url (or a known back office host) to restrict postMessage origins.

2.2. Handshake mechanism

When the extension iframe finishes loading, it must:

  1. Notify OneStock that it is ready

  2. Receive contextual data from OneStock (order ID(s), API URL, signature…)

2.2.1. Step 1 – Extension → OneStock: extension_ready

From your extension JavaScript, as soon as you are ready to receive data, send:

// Example: send 'extension_ready' to the parent window window.parent.postMessage( { type: "extension_ready" }, "*" // or use parent_url for stricter security );

For production, you should replace "*" with the exact parent origin (derived from parent_url), to avoid accepting messages from other origins.

2.2.2. Step 2 – OneStock → Extension: onestock_data

After receiving extension_ready, OneStock sends a message back to your iframe via postMessage with:

  • type: "onestock_data"

  • data: { ... } – payload containing shared data

Use this pattern to receive the message:

let sharedData = {}; window.addEventListener("message", handleMessage); function handleMessage(event) { // Optional but recommended: restrict origin: // if (event.origin !== "https://<your-onestock-backoffice-host>") return; if (!event.data || typeof event.data !== "object") return; if (event.data.type === "onestock_data") { // event.data.data is the OneStock payload (e.g. order_id, order_ids, user info…) sharedData = event.data.data; // Your code here: check signature and save data in context console.log("Received OneStock data:", sharedData); } }

3. Security - Extension signature

If your extension gives access to sensitive external data (e.g. your own APIs), you must:

  1. Validate the OneStock extension signature before establishing a session.

  2. Reject or limit access if the signature is missing or invalid.

3.1. Inputs required for signature verification

To validate the signature you need:

  1. Secret hash

    • Provided by OneStock during extension installation.

    • You will have a different hash for every OneStock instance you are using the extension on (qualif, training, production)

  2. Payload

    • Concatenation of the extension id and user id with two # characters:

      ${extension_id}##${user_id}

      • extension_id – from URL parameters

      • user_id – from URL parameters

  3. Signature

    • Shared in the handshake data (extension_signature in sharedData)

    • Format : t=timestamp,h0=h[0],h1=h[1],h2=h[2]

      where:

      • timestamp – Unix timestamp in seconds

      • h[0] – HMAC-SHA256 of ${timestamp}.${payload} using the latest hash

      • h[1] – same with the previous hash

      • h[2] – same with the oldest hash

3.2. Encryption details

  • Algorithm: HMAC-SHA256

  • Message to sign: ${timestamp}.${payload}

    where:

    • timestamp – taken from the signature string

    • payload${extension_id}##${user_id}

3.3. Example of signature verification in Node.js

Below is an example you can adapt. It:

  • Accepts a list of current and previous keys

  • Rejects signatures older than 6 hours

  • Compares against each h0, h1, h2 value

const crypto = require("crypto"); /** * Attempts to verify the extension signature against a list of keys. * * @param {object} req - Object containing signature, extension_id and user_id. * @param {array} secretKeys - Array of secret keys (current + previous). * @returns {boolean} - True if the signature is valid with any key. */ function verifyWithPreviousKeys(req, secretKeys) { for (const key of secretKeys) { if (checkExtensionSignature(req, key)) { return true; } } return false; } /** * Checks the validity of the extension signature to ensure the request * is from a trusted OneStock source. * * @param {object} req - { extension_signature, extension_id, user_id }. * @param {string} secretKey - Secret key used to generate the signature. * @returns {boolean} - True if the signature is valid, else false. */ function checkExtensionSignature(req, secretKey) { const signatureHeader = req.extension_signature; // e.g. "t=...,h0=...,h1=...,h2=..." const body = `${req.extension_id}##${req.user_id}`; if (!signatureHeader) return false; const signatureParts = signatureHeader.split(","); if (signatureParts.length < 2) return false; const timestamp = signatureParts[0].replace("t=", ""); // Reject if older than 6 hours const nowInSeconds = Math.floor(Date.now() / 1000); if (nowInSeconds - parseInt(timestamp, 10) > 60 * 60 * 6) { return false; } const payload = `${timestamp}.${body}`; const expectedSignature = crypto .createHmac("sha256", secretKey) .update(payload) .digest("hex"); // Compare against each h0, h1, h2... for (let i = 1; i < signatureParts.length; i++) { const h = signatureParts[i].replace(`h${i - 1}=`, ""); if (h === expectedSignature) { return true; } } return false; } // Example usage const req = { extension_signature: "t=timestamp,h0=hash0,h1=eb2a2ab3d29587fba099451925bdbd9637814711c97bc79c6a6e86f898884796", user_id: "user_id", extension_id: "extension_id", }; const secretKeys = ["oldKey1", "oldKey2", "currentKey"]; const isVerified = verifyWithPreviousKeys(req, secretKeys); console.log(`Extension signature verification result: ${isVerified}`);

3.4. Secure your extension

Once the signature validated, you have to implement a session handler of your choice such as JWT with a short expiration date for the token availabilities.

Other options exists depending on your IT availabilities. If your enterprise have an existing Single Sign On server, this is a good option to use and implement and complement to our signature verification for even more security to your data access.


4. Anchors reference

This section defines, per anchor:

  • Where it appears in OneStock

  • How the extension is displayed

  • Additional data you receive

  • Extra messages you can send back to OneStock


4.1. bo.page – custom page in the back office

Behavior

  • Adds a new menu entry in the OneStock back office.

  • The menu entry:

    • Uses the configured icon and name.

Payload

  • You receive:

    • Common fields (api_url, extension_signature, etc.)

    • No additional fields specific to bo.page.

4.1.1. Update back office breadcrumb (v19)

Your extension can update the OneStock breadcrumb when the user navigates inside your page by sending:

window.parent.postMessage( { type: "extension_breadcrumb_update", breadcrumb_items: [ { name: "Books-music-video", path: "/books-music-video", params: "" }, { name: "Blade Runner 2048", path: "/books-music-video/1005199002506", params: "", }, ], }, "*" // restrict to backoffice origin in production );

4.1.2. Listen to breadcrumb clicks (v19)

When a breadcrumb item is clicked in OneStock, your extension receives a navigation event:

window.addEventListener("message", (event) => { if (!event.data || typeof event.data !== "object") return; if (event.data.type === "onestock_navigation") { const { path } = event.data.data; // Your code: route within your extension to the given path console.log("Navigate to path:", path); } });

Example payload from OneStock:

{ "type": "onestock_navigation", "data": { "path": "/books-music-video" } }

4.2. bo.orders.action – bulk action on orders

Behavior

  • Adds a custom bulk action in the back office order list.

  • When the user selects multiple orders and triggers your action:

    • OneStock opens your extension in a modal iframe.

    • You receive the list of order IDs.

Payload

  • In sharedData:

    • Common fields (api_url, extension_signature, …)

    • order_ids – comma‑separated list of selected order IDs

Closing the modal

Your extension can request OneStock to close the modal:

window.parent.postMessage( { type: "extension_close", }, "*" // restrict in production );

4.3. bo.order.action – action on order detail

Behavior

  • Adds a custom action button on the order detail page.

  • When triggered:

    • OneStock opens your extension in a modal iframe.

    • You receive the current order ID.

Payload

  • In sharedData:

    • Common fields (api_url, extension_signature, …)

    • order_id – the current order ID

Closing the modal

Same as for bo.orders.action:

window.parent.postMessage( { type: "extension_close", }, "*" // restrict in production );

4.4. sa.page - custom page on the store application

Behaviour

  • Adds a new menu entry in the store application.

    • You can set the store application extension to be displayed in the menu section of your choice.
      In the bellow configuration we are displaying our extension in the admin section of the menu 0
      The available menus and section can listed in the Backoffice > Store app > Menu configuration page.
      Replace menu_key and menu_section with the correct values depending on your store app configurations.

      { "name": "store-app-extension", "injection_points": [ { "anchor": "sa.page", "icon": "/9j/4Q/+RXhp...", "name": "SA Extension", "path": "/sa-page", "params": { "menu": { "0": "admin", "{{menu_key}}": "{{menu_section}}" } }, "rank": 1, "slug": "sa-main-page" } ], "url": "https://myextensionurl.com", "test_url": "https://myextensionurl.com" }



image-20260429-121933.png
d8ed3ba5-a60c-49ff-be7a-98d8ef222176.png

Payload

  • In sharedData:

    • Common fields (api_url, extension_signature, …)

    • endpoint_id : the id of the stock location


4.5. bo.customer.tab – custom tab on Customer Profile page

Behavior

  • Adds a custom tab on the customer profile page.

  • When triggered:

    • OneStock opens your extension in a tab and you can add your iframe.

    • You receive the customer ID.

Payload

  • In sharedData:

    • Common fields (api_url, extension_signature, …)

    • customerId , customerExternalId


4.6. bo.customer.information – custom block on Customer Profile page

Behavior

  • Adds a custom information block on the customer profile page.

  • When triggered:

    • OneStock opens your extension in a block when the customer profile page is openned

    • You receive the customer ID.

Payload

  • In sharedData:

    • Common fields (api_url, extension_signature, …)

    • customerId , customerExternalId


5. Translating your extension

5.1. Translate the content of your extension

It is up to you to use the lang parameter received in the URL parameters to correctly translate your extension with a library such as i18n.

The lang value available from the Backoffice are:

Lang

Value

Lang

Value

French

fr

English

en

Deutch

de

Italian

it

Spanish

es

Greek

el

Russian

ru

5.2. Backoffice placeholders

For each anchor, the backoffice will display its name by default in the menu for a page, or in a button for an action. You can add translation keys in the backoffice translation configuration page.

  • Key format : extensions.{extension_name}.{slug}

  • Where extension_name (spaces are allowed) is the name of your extension

  • Where slug is the slug for your anchor.

Ex: If you have an extension called Hello World with 3 anchors:

  • bo.page : with a slug page

  • bo.orders.action : with a slug orders-action

  • bo.order.action : with a slug order-action

You can create 3 translation keys:

  • extensions.Hello World.page

  • extensions.Hello World.orders-action

  • extensions.Hello World.order-action

image-20260130-140705.png

 


6. Using OneStock API with your extensions

To use OneStock APIs the extension must manage the session after the signature verification with the security approach of your choice, and then expose a backend proxy to call OneStock APIs using and application user that would be saved in your project environment.