Technical guide

Technical guide

Architecture overview

Main components

  • Shopify Public App: OneStock Connector for Shopify

  • OneStock APIs (products, inventory, orders)

  • OneStock webhooks

  • SFTP integration

  • Shopify webhooks for real-time synchronization

image-20251125-165836.png

Shipping/BOPIS/PUDO order workflow

  • A customer places an order on Shopify.

  • The OneStock Connector transfers the order to OneStock OMS.

  • OneStock determines the optimal fulfillment location.

  • The order is fulfilled (shipped or prepared for pickup).

  • Status updates and tracking are sent back to Shopify.

image-20260121-100849.png

Shipping order with delivery promise workflow

  • A customer places an order on Shopify with delivery promise information provided trhough OneStock Connector by OneStock OMS.

  • The OneStock Connector transfers the order to OneStock OMS.

  • OneStock determines the optimal fulfillment location.

  • The order is fulfilled (shipped or prepared for pickup).

  • Status updates and tracking are sent back to Shopify.

image-20260121-102533.png

Product synchronization

Product integration overview

OneStock connector only integrates products with an active status in Shopify and with an EAN.

🔍 How it works technically

The product synchronization is only manual. It happens only when you click on the synchronize button in Shopify admin on the products area.

When you press the sync button, the connector performs the following steps:

  1. Fetches a batch of 15 products from Shopify.

  2. Extracts all variants associated with these products.

  3. Sends a PATCH request to OneStock to update or create the corresponding items.

This process is repeated until all selected products and variants have been processed.

Products workflow

image-20241212-131410.png
Manual trigger
query ($numProducts: Int!, $cursor: String) { products(first: $numProducts, after: $cursor) { nodes { id title handle description category { fullName name } tags media(first: 50) { edges { node { preview { image { url } } } } } metafields(first: 50) { edges { node { id namespace key value } } } variants(first: 100) { edges { node { displayName id sku barcode inventoryItem { id measurement { weight { unit value } } } selectedOptions { name value } product { id } barcode metafields(first: 50) { edges { node { id namespace key value } } } inventoryItem { measurement { weight { unit value } } } price image { id width url } } } } } pageInfo { hasNextPage endCursor } } }

Please do not use the actual synchronize button on the configuration, it is not working. Analyze by OneStock team in progress.

Currently, when multiple systems import product data, the Onestock connector performs a full overwrite of existing data. As a result, information coming from other systems may be replaced in the OneStock OMS

Stock synchronization

Stock integration overview

OneStock connector requests OneStock to share an image of the stock with it so that it can then be made available for sale on Shopify.

🔍 How it works technically

  1. The OneStock connector sends a POST /stock_export request to OneStock. This export can either be:

    • Triggered manually from the connector, or

    • Scheduled to run automatically (full or delta exports) at specific times.

      • For configuration details, see the internal documentation

  2. When the stock export is ready on the OneStock side, OneStock sends a stock_export_completed webhook to the OneStock connector.

  3. Once the export is ready, the connector reads the generated stock file and processes the first 250 lines. For these lines, the connector calls Shopify’s InventoryItem query to retrieve the corresponding InventoryItems needed to update stock in Shopify.

    • We expect to receive one InventoryItem per SKU.

      • If this is not the case, it means there are duplicate SKUs in Shopify (the same SKU is linked to two different InventoryItem records).

      • In that scenario, the connector will attempt to perform two stock updates on the same SKU, each targeting a different InventoryItem.

Because we are limited to 250 InventoryItem imports per batch, duplicates can cause an issue: some SKUs present in the first 250 lines of the stock file may not be included in the InventoryItem response from Shopify and will therefore be excluded from the stock import.

  1. Finally, the OneStock connector calls the Shopify inventorySetQuantities mutation (Admin GraphQL API) to import stock levels into Shopify.
    The connector updates the available type for each InventoryItem quantity at the relevant location.

Stocks workflow

image-20251124-091248.png
mutation inventorySetOnHandQuantities($input: InventorySetOnHandQuantitiesInput!) { inventorySetOnHandQuantities(input: $input) { userErrors { field message code } inventoryAdjustmentGroup { id changes { name delta quantityAfterChange } reason referenceDocumentUri } } }

We higly recommend to only have one InventoryItem per sku

Order synchronization

Order integration overview

Refer to this page to have more detailed information.

🔍 How it works technically

  • If order synchronization is activated, OneStock connector create a webhook to listen to 3 topics in OneStock:

    • order_state_changed

    • parcel_state_changed

    • line_item_group_state_changed

  • We also start listenning ORDERS_CREATE and

    ORDERS_UPDATED topics from Shopify.

If synchronise order is disabled, the impacts are:

  • Stopping the import of orders from Shopify

  • Stopping the creation of OneStock exports for integration into Shopify

  • Stopping the update of orders

Activating order synchronization does not result in the import of orders that were created in Shopify prior to activation. Previously triggered events are not replayed or recovered.

  • When Shopify triggers the webhook ORDERS_CREATE. OneStock connector receives it. Please note this means that the order has not yet been paid for.

{ "id": 6323276120392, "admin_graphql_api_id": "gid://shopify/Order/6323276120392", "app_id": 580111, "browser_ip": "93.93.41.185", "buyer_accepts_marketing": false, "cancel_reason": null, "cancelled_at": null, "cart_token": "Z2NwLWV1cm9wZS13ZXN0NDowMUpEVkczRU5YOEVZRjNBWkNQOTA5MDFCRw", "checkout_id": 49075069321544, "checkout_token": "0a97bf4271d8b12e8d4202cea8c77091", "client_details": { "accept_language": "en-FR", "browser_height": null, "browser_ip": "93.93.41.185", "browser_width": null, "session_hash": null, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" }, "closed_at": null, "company": null, "confirmation_number": "8VPC5JHPK", "confirmed": true, "contact_email": null, "created_at": "2024-11-29T03:20:45-05:00", "currency": "EUR", "current_shipping_price_set": { "shop_money": { "amount": "7.06", "currency_code": "EUR" }, "presentment_money": { "amount": "10.00", "currency_code": "SGD" } }, "current_subtotal_price": "10.59", "current_subtotal_price_set": { "shop_money": { "amount": "10.59", "currency_code": "EUR" }, "presentment_money": { "amount": "15.00", "currency_code": "SGD" } }, "current_total_additional_fees_set": null, "current_total_discounts": "0.00", "current_total_discounts_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "current_total_duties_set": null, "current_total_price": "17.65", "current_total_price_set": { "shop_money": { "amount": "17.65", "currency_code": "EUR" }, "presentment_money": { "amount": "25.00", "currency_code": "SGD" } }, "current_total_tax": "0.00", "current_total_tax_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "customer_locale": "en-FR", "device_id": null, "discount_codes": [], "duties_included": false, "email": "", "estimated_taxes": false, "financial_status": "paid", "fulfillment_status": null, "landing_site": "/", "landing_site_ref": null, "location_id": null, "merchant_business_entity_id": "MTg1NTczNTAxMjU2", "merchant_of_record_app_id": null, "name": "#1067", "note": null, "note_attributes": [], "number": 67, "order_number": 1067, "order_status_url": "https://lovisa-os.myshopify.com/85573501256/orders/4e7cd5c55df773da587eefb52e7fb27b/authenticate?key=edfa0cb03ee110009147e74a98073325", "original_total_additional_fees_set": null, "original_total_duties_set": null, "payment_gateway_names": [ "bogus" ], "phone": "+6565382300", "po_number": null, "presentment_currency": "SGD", "processed_at": "2024-11-29T03:20:43-05:00", "reference": "8ac05a5e7417e1e7cacca7f2643116b2", "referring_site": "", "source_identifier": "8ac05a5e7417e1e7cacca7f2643116b2", "source_name": "web", "source_url": null, "subtotal_price": "10.59", "subtotal_price_set": { "shop_money": { "amount": "10.59", "currency_code": "EUR" }, "presentment_money": { "amount": "15.00", "currency_code": "SGD" } }, "tags": "", "tax_exempt": false, "tax_lines": [], "taxes_included": true, "test": true, "token": "4e7cd5c55df773da587eefb52e7fb27b", "total_cash_rounding_payment_adjustment_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "total_cash_rounding_refund_adjustment_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "total_discounts": "0.00", "total_discounts_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "total_line_items_price": "10.59", "total_line_items_price_set": { "shop_money": { "amount": "10.59", "currency_code": "EUR" }, "presentment_money": { "amount": "15.00", "currency_code": "SGD" } }, "total_outstanding": "0.00", "total_price": "17.65", "total_price_set": { "shop_money": { "amount": "17.65", "currency_code": "EUR" }, "presentment_money": { "amount": "25.00", "currency_code": "SGD" } }, "total_shipping_price_set": { "shop_money": { "amount": "7.06", "currency_code": "EUR" }, "presentment_money": { "amount": "10.00", "currency_code": "SGD" } }, "total_tax": "0.00", "total_tax_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "total_tip_received": "0.00", "total_weight": 0, "updated_at": "2024-11-29T03:20:46-05:00", "user_id": null, "billing_address": { "first_name": "T", "address1": "149b Towner Road", "phone": null, "city": "Singapore", "zip": "322149", "province": null, "country": "Singapore", "last_name": "P", "address2": null, "company": null, "latitude": null, "longitude": null, "name": "T P", "country_code": "SG", "province_code": null }, "customer": { "id": 8259738108232, "email": null, "created_at": "2024-11-22T08:45:56-05:00", "updated_at": "2024-11-29T03:20:45-05:00", "first_name": "T", "last_name": "P", "state": "disabled", "note": null, "verified_email": true, "multipass_identifier": null, "tax_exempt": false, "phone": "+6565382300", "email_marketing_consent": null, "sms_marketing_consent": { "state": "not_subscribed", "opt_in_level": "single_opt_in", "consent_updated_at": null, "consent_collected_from": "SHOPIFY" }, "tags": "", "currency": "EUR", "tax_exemptions": [], "admin_graphql_api_id": "gid://shopify/Customer/8259738108232", "default_address": { "id": 10682761183560, "customer_id": 8259738108232, "first_name": "T", "last_name": "P", "company": null, "address1": "149b Towner Road", "address2": null, "city": "Singapore", "province": null, "country": "Singapore", "zip": "322149", "phone": null, "name": "T P", "province_code": null, "country_code": "SG", "country_name": "Singapore", "default": true } }, "discount_applications": [], "fulfillments": [], "line_items": [ { "id": 16137270231368, "admin_graphql_api_id": "gid://shopify/LineItem/16137270231368", "attributed_staffs": [], "current_quantity": 1, "fulfillable_quantity": 1, "fulfillment_service": "manual", "fulfillment_status": null, "gift_card": false, "grams": 0, "name": "Product1000000003 - First1000000003", "price": "10.59", "price_set": { "shop_money": { "amount": "10.59", "currency_code": "EUR" }, "presentment_money": { "amount": "15.00", "currency_code": "SGD" } }, "product_exists": true, "product_id": 9184123388232, "properties": [], "quantity": 1, "requires_shipping": true, "sku": "1000000003", "taxable": true, "title": "Product1000000003", "total_discount": "0.00", "total_discount_set": { "shop_money": { "amount": "0.00", "currency_code": "EUR" }, "presentment_money": { "amount": "0.00", "currency_code": "SGD" } }, "variant_id": 49277081944392, "variant_inventory_management": "shopify", "variant_title": "First1000000003", "vendor": "Burton", "tax_lines": [], "duties": [], "discount_allocations": [] } ], "payment_terms": null, "refunds": [], "shipping_address": { "first_name": "T", "address1": "149b Towner Road", "phone": null, "city": "Singapore", "zip": "322149", "province": null, "country": "Singapore", "last_name": "P", "address2": null, "company": null, "latitude": 1.3177874, "longitude": 103.8600242, "name": "T P", "country_code": "SG", "province_code": null }, "shipping_lines": [ { "id": 5341247013192, "carrier_identifier": "52b5979428263ca9ad092cf9fb13ee71", "code": "STANDARD_SG", "current_discounted_price_set": { "shop_money": { "amount": "7.06", "currency_code": "EUR" }, "presentment_money": { "amount": "10.00", "currency_code": "SGD" } }, "discounted_price": "7.06", "discounted_price_set": { "shop_money": { "amount": "7.06", "currency_code": "EUR" }, "presentment_money": { "amount": "10.00", "currency_code": "SGD" } }, "is_removed": false, "phone": null, "price": "7.06", "price_set": { "shop_money": { "amount": "7.06", "currency_code": "EUR" }, "presentment_money": { "amount": "10.00", "currency_code": "SGD" } }, "requested_fulfillment_service_id": null, "source": "Onestock Shipping Rate Provider", "title": "Home delivery Standard", "tax_lines": [], "discount_allocations": [] } ], "returns": [] }
  • OneStock connector retrieve the order payload, normalizes it, and sends it to OneStock through aPOST/ordersif the order does not exist yet.

{ "id (id)":"6323276120392", "types (ffs hardcoded, ""ckc"" added if there is ckc DP)":[ "ffs" ], "information":{ "shopify_shop":"lovisa-os.myshopify.com", "order_note":"", "notes_attributes":[] }, "sales_channel (in config)":"lovisa_website_sg", "delivery":{ "destination":{ "address":{ "city (shopifyOrder.shipping_address.city)":"Singapore", "contact":{ "title":"M/Mrs.", "first_name (shopifyOrder.shipping_address.first_name)":"T", "last_name (shopifyOrder.shipping_address.last_name)":"P", "phone_number (shopifyOrder.shipping_address?.phone ?? shopifyOrder.customer?.phone ?? shopifyOrder.phone)":"+6565382300", "email (shopifyOrder.customer?.email ?? shopifyOrder.email ?? "")":"" }, "lines (shopifyOrder.shipping_address.address1 & shopifyOrder.shipping_address.address2)":[ "149b Towner Road" ], "coordinates":{ "lat (shopifyOrder.shipping_address.latitude)":1.3177874, "lon (shopifyOrder.shipping_address.longitude)":103.8600242 }, "regions":{ "country":{ "code (shopifyOrder.shipping_address.country_code)":"SG" } }, "zip_code (shopifyOrder.shipping_address.zip)":"322149" } }, "type (deliveryPromise.delivery_routes[0])":"express" }, "customer":{ "email (shopifyOrder.customer.email ?? "")":"", "first_name (shopifyOrder.customer.first_name)":"T", "information":{ }, "last_name (shopifyOrder.customer.last_name)":"P", "phone_number (shopifyOrder.shipping_address?.phone ?? shopifyOrder.customer?.phone ?? shopifyOrder.phone)":"+6565382300", "title":"M/Mrs." }, "pricing_details":{ "currency (shopifyOrder.currency)":"EUR", "address":{ "city (shopifyOrder.billing_address.city)":"Singapore", "contact":{ "title":"M/Mrs.", "first_name (shopifyOrder.billing_address.first_name)":"T", "last_name (shopifyOrder.customer.last_name)":"P", "phone_number (shopifyOrder.billing_address.phone)":"+6565382300", "email (shopifyOrder.customer.email)":null }, "lines (shopifyOrder.billing_address.address1 & shopifyOrder.billing_address.address2)":[ "149b Towner Road" ], "coordinates":{ "lat (shopifyOrder.billing_address.latitude)":1.3177874, "lon (shopifyOrder.billing_address.longitude)":103.8600242 }, "regions":{ "country":{ "code (shopifyOrder.billing_address.country_code)":"SG" } }, "zip_code (shopifyOrder.billing_address.zip)":"322149" }, "original_price (shopifyOrder.total_line_items_price)":10.59, "price (shopifyOrder.total_price)":17.65 }, "order_items":[ { "item_id (lineItem.sku)":"1000000003", "quantity (lineItem.quantity)":1, "pricing_details":{ "currency (lineItem.price_set.shop_money.currency_code)":"EUR", "original_price (lineItem.price * lineItem.quantity)":10.59, "price ((lineItem.price * lineItem.quantity))":10.59, "original_unit_price (lineItem.price)":10.59, "unit_price (lineItem.price)":10.59 }, "information":{ "shopify_line_item_id (lineItem.id)":16137270231368, "shopify_sku (lineItem.sku)":"1000000003", "properties (lineItem.properties)":[ ], "requires_shipping (lineItem.requires_shipping)":true } } ], "shipping_fees":[ { "original_price (shopifyOrder.total_shipping_price_set.shop_money.amount)":7.06, "price (shopifyOrder.total_shipping_price_set.shop_money.amount)":7.06 } ], "original_ruleset_chaining_id (in config if defined else omitted)":"home_delivery_singapore", "delivery_promise":{ "original_delivery_option (deliveryPromise)":{ "carbon_footprint":10, "cost":10, "cutoff":1733090400, "delivery_method":"Home Delivery Standard Singapore", "destination":{ "location":{ "country":"SG", "zip_code":"322149" } }, "eta_end":1733832001, "eta_start":1733367597, "metric_values":{ "shipping_fees":5 }, "shipment_number":1, "delivery_routes":[ { "carrier":{ "name":"dhlv2", "option":"express" }, "items":[ { "id":"1000000003", "quantity":1 } ] } ], "status":"valid" }, "sent_delivery_option":{ "eta_start (deliveryPromise.eta_start)":1733367597, "eta_end (deliveryPromise.eta_end)":1733832001 } } }
  • When Shopify triggers the webhook ORDERS_UPDATED. OneStock connector receives it and updates the information field of the order in OneStock

 

Status synchronization

Status overview

Changes to order and return statuses are retrieved by the OneStock connector and transmitted to Shopify.

🔍 How it works technically

  • Thanks to information from topics order_state_changed, parcel_state_changed, line_item_group_state_changed, OneStock connector can update orders in Shopify.

  • Please note OneStock connector create fulfillment by API and Shopify update the Fulfillment Order.

Topic

Description

Topic

Description

order_state_changed

Only removed, fulfilled, collectable states are listened to. If the order is at one of those state, the connector checks if there are items that need to be hold. In case there are items that need to be hold removed, it applies a fulfillment on_hold on the related fulfillment order. In case there are items that need to be pickup prepaped, it applies a fulfillment ready_to_pickupon the related fulfillment order.

parcel_state_changed

Only dispatched and collected states are listened to. If a parcel is at one of those state, the OneStock connector create a fulfillment to fulfill for the related line items.

line_item_group_state_changed

Only removed state is listened to. If the line item group is at this state, it applies a fulfillment on_hold on the related fulfillment order.

The statuses listed above cannot be modified. Your workflow in OneStock must have these statuses in order to update Shopify correctly.

Delivery promise front integration

Delivery promise overview

OneStock connector allows you to use on your website the delivery promise module.

🔍 How it works technically

  • When we arrive at the checkout page, we retrieve the first delivery group that is offered to us by Shopify.

    • A DeliveryGroup is a set of cart lines that share the same delivery options.

    • A DeliveryGroup therefore contains:

      • targetedCartLines (the cart lines included in the group)

      • deliveryOptions (the delivery methods available for this group)

      • the isDeliveryRequired property

  • Using the information provided in targetedCartLines and the address entered by the customer at checkout, the OneStock connector calls the OneStock delivery_promise endpoint.

  • The response from the delivery_promise API is then used to display the availability of the delivery method in the checkout.

Delivery promise workflow

image-20251124-141018.png

App proxy routes

OneStock Connector has two app proxy routes:

  • For delivery promises.

  • For stock export.

App proxies take requests to Shopify links, and redirect them to external links. This allows you to fetch and display data on a merchant's store from an external source. For more information about proxy app, refer to app proxies.

Use delivery_promise when you need delivery estimates. Use stock_export when you need a stock quantity snapshot. Choose based on the data you need to display.

These APIs are designed for use on Product Detail Pages (PDP) or later in the funnel (cart, checkout). They should not be called on Product Listing Pages (PLP) due to the volume of requests it would generate.

Delivery promise

POST /apps/onestock/proxy/delivery_promises

Returns estimates about the delivery of a product or a basket. A list of delivery options may be requested, providing the most detailed information possible about the delivery location. Based on these parameters and configurations, OneStock will estimate the delivery and return this estimate.

To learn more about the payload field, refer to OneStock API documentation

fetch('/apps/onestock/proxy/delivery_promises', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { "payload": { "line_items": [ { "item_id": "1000000000", "qty": 1 } ], "sales_channel": "sc_1", "delivery_options": [ { "delivery_method": "ckc", "destination": { "endpoint_id": "store_001" } }, { "delivery_method": "SDD", "destination": { "location": { "country": "US", "state": "NY", "zip_code": "11378" } } } ], "fields": [ "delivery_options.delivery_routes", "delivery_options.cost" ] } } ) })
{ "delivery_options": [ { "carbon_footprint": 0, "cost": 0, "delivery_method": "ckc", "destination": { "endpoint_id": "store_002" }, "eta_end": 1716200077, "eta_start": 1716200077, "shipment_number": 0, "status": "valid" }, { "carbon_footprint": 0, "cost": 0, "delivery_method": "ckc", "destination": { "endpoint_id": "store_004" }, "reason": "no_eligible_route", "shipment_number": 0, "status": "none" } ] }

Stock request

POST /apps/onestock/proxy/stock_export

Stock exports allow fetching a snapshot of stock at the current point in time.

To learn more about the payload field, refer to OneStock API documentation

fetch('/apps/onestock/proxy/stock_export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify( { "payload": { "aggregates": { "all": { "deduction": { "attributes": [ "not_in_stock" ] }, "item_filter": { "request_name": "all_ids" }, "endpoint_filter": { "request_name": "all_ids" } } } } } ) })
{ "stocks": [ { "item_id": "1000000000", "endpoint_id": "store_003", "quantity": 22, "type": "on_hand" }, { "item_id": "1000000000", "endpoint_id": "store_001", "quantity": 22, "type": "on_hand" }, { "item_id": "1000000000", "endpoint_id": "store_002", "quantity": 22, "type": "on_hand" } ] }