[{"data":1,"prerenderedAt":722},["ShallowReactive",2],{"blog-b2b-order-sync-erp-en":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":25,"sources":35,"cover":71,"og":72,"expertise":73,"locales":74,"lang":75,"title":78,"description":79,"coverAlt":80,"metaTitle":81,"takeaways":82,"faq":88,"toc":104,"blocks":132,"others":449},"b2b-order-sync-erp","2026-10-08",8,"web",[9,10,11,12,13],"ERP integration","SAP","Idempotency","Outbox pattern","Reconciliation",[15,16,17,18,19,20,21,22,23,24],"order sync between shop and ERP","sap sales order api","odata sales order s\u002F4hana","idoc orders05","transactional outbox pattern","idempotency key","erp reconciliation report","sap event mesh sales order created","bapi_salesorder_createfromdat2","shop erp order integration",[26,29,32],{"name":27,"url":28},"Enterprise resource planning","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FEnterprise_resource_planning",{"name":30,"url":31},"Idempotence","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FIdempotence",{"name":33,"url":34},"Transactional outbox pattern","https:\u002F\u002Fmicroservices.io\u002Fpatterns\u002Fdata\u002Ftransactional-outbox.html",[36,39,42,45,48,51,54,57,59,62,65,68],{"title":37,"url":38},"SAP Community: posting sales orders into S\u002F4HANA Cloud via API (2018)","https:\u002F\u002Fcommunity.sap.com\u002Ft5\u002Fenterprise-resource-planning-blog-posts-by-sap\u002Fposting-sales-orders-into-s-4hana-cloud-api-part-1\u002Fba-p\u002F13350199",{"title":40,"url":41},"SAP Community: S\u002F4HANA OData V4 APIs quick reference (15 Feb 2026)","https:\u002F\u002Fcommunity.sap.com\u002Ft5\u002Ffinancial-management-blog-posts-by-sap\u002Fsap-s-4-hana-odata-v4-apis-available-quick-reference-and-available-in-sap\u002Fba-p\u002F14328827",{"title":43,"url":44},"SAP Help: EDI inbound processing, sales order","https:\u002F\u002Fhelp.sap.com\u002Fdoc\u002Fsaphelp_nw70\u002F7.0.12\u002Fja-JP\u002Fdc\u002F6b7cac43d711d1893e0000e8323c4f\u002Fcontent.htm",{"title":46,"url":47},"SAP Help: ALE inbound processing","https:\u002F\u002Fhelp.sap.com\u002Fdoc\u002Fsaphelp_nw75\u002F7.5.5\u002Fen-US\u002F4d\u002F51189400071c86e10000000a42189b\u002Fcontent.htm",{"title":49,"url":50},"SAP Community: custom fields in sales document BAPIs (2024)","https:\u002F\u002Fcommunity.sap.com\u002Ft5\u002Fenterprise-resource-planning-blog-posts-by-sap\u002Fhow-to-use-custom-fields-in-sales-document-bapis\u002Fba-p\u002F13721645",{"title":52,"url":53},"Microsoft Learn: Business Central API v2.0, create salesOrders","https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fdynamics365\u002Fbusiness-central\u002Fdev-itpro\u002Fapi-reference\u002Fv2.0\u002Fapi\u002Fdynamics_salesorder_create",{"title":55,"url":56},"Infor Developer Portal: get all sales orders","https:\u002F\u002Fdeveloper.infor.com\u002Fbusiness-obj-v1\u002Fsales-order\u002Fget-all-sales-orders",{"title":58,"url":34},"microservices.io: transactional outbox",{"title":60,"url":61},"Stripe API reference: idempotent requests","https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fidempotent_requests",{"title":63,"url":64},"Confluent: Kafka message delivery guarantees","https:\u002F\u002Fdocs.confluent.io\u002Fkafka\u002Fdesign\u002Fdelivery-semantics.html",{"title":66,"url":67},"Mercari Engineering: payment reconciliation (2021)","https:\u002F\u002Fengineering.mercari.com\u002Fen\u002Fblog\u002Fentry\u002F20211222-1df9e3a553\u002F",{"title":69,"url":70},"OpenTelemetry: messaging spans semantic conventions","https:\u002F\u002Fopentelemetry.io\u002Fdocs\u002Fspecs\u002Fsemconv\u002Fmessaging\u002Fmessaging-spans\u002F","\u002Fimages\u002Fblog\u002Fb2b-order-sync-erp\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fb2b-order-sync-erp\u002Fog.jpg","b2b-ecommerce-developer",[75,76,77],"en","de","hu","Order sync between shop and ERP: SAP APIs, idempotency and reconciliation","How to sync orders between a B2B shop and the ERP: SAP OData, IDoc and BAPI options, the outbox pattern, idempotency keys and routine reconciliation.","Two boxes joined by a loop of arrows carrying an envelope and a receipt.","Order sync between shop and ERP · Balázs Csorba",[83,84,85,86,87],"Order sync is a loop of transactional documents: the shop order becomes an ERP sales order, and confirmations, deliveries and invoices flow back; cancellations and returns are new documents, not deletes.","In SAP the main routes are the OData sales order API, IDocs with message type ORDERS and basic type ORDERS05, and BAPI_SALESORDER_CREATEFROMDAT2, which needs an explicit BAPI_TRANSACTION_COMMIT.","Write the order and an outbox row in one database transaction, accept that the relay may publish twice, and make the ERP writer idempotent on the shop’s order reference.","Exactly-once delivery rarely survives the ERP boundary; Kafka defaults to at-least-once, so design for duplicates instead of hoping to avoid them.","Reconciliation is routine work: compare shop and ERP states on a schedule, classify the drift, and repair through the same writer that handles normal orders.",[89,92,95,98,101],{"q":90,"a":91},"How do you sync orders between a web shop and SAP?","Common routes are the OData sales order API, inbound IDocs with message type ORDERS and basic type ORDERS05, or the BAPI BAPI_SALESORDER_CREATEFROMDAT2 over RFC. Choose by coupling and latency needs, then add an outbox in the shop, an idempotent writer keyed on the shop order reference, and a scheduled reconciliation.",{"q":93,"a":94},"How do you prevent duplicate sales orders in the ERP?","Carry the shop’s order reference into a field the ERP can check, such as the customer purchase order number on an SAP sales order or externalDocumentNumber in Business Central, check it before creating, and keep a processed-message table in the consumer. Retries then find the existing order instead of creating a second one.",{"q":96,"a":97},"What is the transactional outbox pattern?","The shop writes the order and a message row into its own database in the same transaction. A separate relay publishes the message later and may publish it more than once, so the consumer must be idempotent. The pattern removes the gap where an order is saved but its message is lost, or the reverse.",{"q":99,"a":100},"Is exactly-once delivery possible between a shop and an ERP?","In practice, no. Kafka’s transactional exactly-once applies only when the whole pipeline stays in Kafka, and an ERP call is outside it. Design for at-least-once delivery with idempotent consumers and natural keys in the ERP, then reconcile to catch what still slips through.",{"q":102,"a":103},"What should an order reconciliation job check?","Orders that exist in the shop but not in the ERP, orders in the ERP that the shop does not know, and orders in both with mismatched details such as quantities, prices or status. Record each finding, queue a repair command, and run the repair through the same canonical writer as normal orders.",[105,108,111,114,117,120,123,126,129],{"id":106,"title":107},"what-order-sync-covers","What does order sync cover?",{"id":109,"title":110},"the-order-lifecycle-between-shop-and-erp","Who owns which order state?",{"id":112,"title":113},"sap-integration-options","Which SAP integration options exist for orders?",{"id":115,"title":116},"beyond-sap","What about Dynamics 365, Infor and other ERPs?",{"id":118,"title":119},"delivery-guarantees-batch-event-queue","Which delivery guarantees can you actually get?",{"id":121,"title":122},"idempotency-and-the-outbox","How do the outbox and idempotency work together?",{"id":124,"title":125},"failure-modes-and-reconciliation","Which failures happen, and how does reconciliation catch them?",{"id":127,"title":128},"testing-observability-and-checklist","Testing, observability and a go-live checklist",{"id":130,"title":131},"sources","Sources",[133,141,150,153,156,162,163,166,174,177,178,181,217,220,277,278,301,304,305,312,315,318,319,325,329,331,333,343,350,351,354,361,373,374,380,398,410,411],{"type":134,"content":135},"paragraph",[136,140],{"tag":137,"children":138},"strong",[139],"Order sync between shop and ERP"," is the loop of transactional documents that turns a web shop order into an ERP sales order and brings confirmations, delivery status and invoices back. Unlike product data sync, a failure here is not a wrong price on a page; it is a duplicate sales order, a lost order or an invoice that does not match, which makes it a finance incident.",{"type":134,"content":142},[143,144,149],"This article covers who owns which state, the SAP integration options and their equivalents in Dynamics 365 Business Central and Infor, delivery guarantees, the outbox pattern with idempotent consumers, failure modes and reconciliation, and testing and observability. Product master data sync is a separate topic, covered in ",{"tag":145,"to":146,"children":147},"link","\u002Fblog\u002Fpimcore-erp-delta-sync",[148],"the Pimcore ERP delta sync",".",{"type":151,"level":152,"id":106,"text":107},"heading",2,{"type":134,"content":154},[155],"It covers every transactional document between the two systems: the order going out, and the order confirmation, availability and price corrections, delivery status and invoices coming back. Cancellations and returns are not deletes; they are new compensating documents that travel through the same loop.",{"type":157,"src":158,"alt":159,"wide":160,"caption":161},"image","\u002Fimages\u002Fblog\u002Fb2b-order-sync-erp\u002Fdiagram-4.webp","Order document loop between shop and ERP Five documents from left to right: the shop order goes from shop to ERP; the ERP sales order is the record of truth; the order confirmation, the delivery status and the invoice flow back from ERP to shop. Cancellations and returns are new compensating documents in the same loop, not deletions.",true,"The order loop: one document goes out, several come back, and corrections are new documents rather than edits to old ones.",{"type":151,"level":152,"id":109,"text":110},{"type":134,"content":164},[165],"The shop owns the entry point and the customer’s view; the ERP owns pricing, stock allocation, credit checks, delivery and invoicing. “Synced” therefore means something different per field: the shop’s order number is shop-owned and must reach the ERP unchanged, while the confirmed delivery date is ERP-owned and the shop may only display it.",{"type":134,"content":167},[168,169,173],"Write that ownership down per field before writing code. It decides which side wins in a conflict, which side may correct a value, and what a support assistant can honestly say; a customer-facing assistant can only answer “where is my order” if this loop is trustworthy, see ",{"tag":145,"to":170,"children":171},"\u002Fblog\u002Fai-assistant-b2b-shop-customer-support",[172],"AI support for B2B shops",". In more than ten years of Spryker and Pimcore projects, I have seen product sync problems show up as wrong prices on a page and order sync problems show up in the finance department, which is why this post spends most of its time on idempotency and reconciliation.",{"type":134,"content":175},[176],"Partial outcomes are the normal case in B2B, not the exception. The ERP may confirm 80 of 100 pieces now and the rest next week, replace a discontinued item, or adjust a price because a contract changed. The shop has to show those changes as they are, which means the order status model needs line-level states and the confirmation message needs to update lines, not overwrite the whole order.",{"type":151,"level":152,"id":112,"text":113},{"type":134,"content":179},[180],"Three families: the OData sales order API, IDocs over ALE, and BAPIs over RFC. Each wins in a different setting, and many landscapes run more than one.",{"type":182,"ordered":183,"items":184},"list",false,[185,198,209],[186,189,190,194,195,197],{"tag":137,"children":187},[188],"OData API:"," the sales order service API_SALES_ORDER_SRV supports create, read, update and delete; a 2018 SAP Community walkthrough notes that items can only be posted through a deep insert of header and items together (",{"tag":191,"href":38,"children":192},"a",[193],"SAP Community","), so check the behavior on your release. The direction of travel is OData V4: SAP listed 363 V4 APIs for S\u002F4HANA on-premise as of 15 February 2026 (",{"tag":191,"href":41,"children":196},[193],").",[199,202,203,206,207,197],{"tag":137,"children":200},[201],"IDoc and ALE:"," the classic EDI route uses message type ORDERS, basic type ORDERS05, process code ORDE and inbound function IDOC_INPUT_ORDERS, configured per customer partner profile; errors raise a work item, and processing can be immediate or in the background (",{"tag":191,"href":44,"children":204},[205],"SAP Help","). ALE also supports serialization so dependent messages post in order (",{"tag":191,"href":47,"children":208},[205],[210,213,214,216],{"tag":137,"children":211},[212],"BAPI over RFC:"," BAPI_SALESORDER_CREATEFROMDAT2 creates the order from header, partner and item tables, returns the document number in SALESDOCUMENT and messages in RETURN, and does not commit; the caller must run BAPI_TRANSACTION_COMMIT (",{"tag":191,"href":50,"children":215},[193],"). A forgotten commit is a classic “the API said OK but nothing is there” incident.",{"type":134,"content":218},[219],"How to choose: if the shop runs in the cloud and needs an answer in the checkout, the OData API is the natural fit. If the customer already sends EDI orders and the SAP team runs IDoc monitoring, reusing ORDERS05 keeps one operating model. BAPIs fit middleware that already sits next to SAP with RFC access. Whatever you pick, the dedupe key and the reconciliation job stay the same, so treat the transport as replaceable.",{"type":221,"head":222,"rows":233},"table",[223,225,227,229,231],[224],"Option",[226],"Latency",[228],"Coupling",[230],"Delivery semantics",[232],"Typical fit",[234,245,256,266],[235,237,239,241,243],[236],"OData API (V2 or V4)",[238],"synchronous, seconds",[240],"direct HTTP call",[242],"caller retries; dedupe on a natural key",[244],"web shops, cloud ERP",[246,248,250,252,254],[247],"IDoc over ALE",[249],"queued, minutes",[251],"asynchronous, partner profiles",[253],"status records, reprocessing",[255],"EDI, high volume, on-premise",[257,259,260,262,264],[258],"BAPI over RFC",[238],[261],"tight, needs RFC connectivity",[263],"explicit commit; caller handles retries",[265],"middleware close to SAP",[267,269,271,273,275],[268],"Batch file",[270],"scheduled, hours",[272],"loose",[274],"reruns, file-level dedupe",[276],"low volume, legacy",{"type":151,"level":152,"id":115,"text":116},{"type":134,"content":279},[280,281,285,286,289,290,293,294,297,298,197],"The shapes are similar, and the dedupe anchor is the detail to look for first. Business Central creates sales orders with ",{"tag":282,"children":283},"code",[284],"POST \u002Fapi\u002Fv2.0\u002Fcompanies({id})\u002FsalesOrders"," and returns 201 Created; its ",{"tag":282,"children":287},[288],"externalDocumentNumber"," field is the natural place for the shop’s order reference (",{"tag":191,"href":53,"children":291},[292],"Microsoft Learn","). Infor exposes sales orders through the ION API Gateway with OAuth 2.0; its business object API paginates with a default page size of 100 and a maximum of 20,000, and error payloads carry a ",{"tag":282,"children":295},[296],"retryable"," flag (",{"tag":191,"href":56,"children":299},[300],"Infor",{"type":134,"content":302},[303],"Portability across ERPs is cheaper when the shop talks to one internal order port and each ERP gets a thin adapter. The port defines the canonical order, the dedupe key and the status vocabulary; the adapter maps fields and errors. That is also where a retryable flag such as Infor’s can feed the retry policy directly.",{"type":151,"level":152,"id":118,"text":119},{"type":134,"content":306},[307,308,311],"At-least-once, made safe by idempotency. Kafka’s default is at-least-once; since version 0.11.0.0 the idempotent producer removes duplicates caused by its own retries using a producer id and sequence numbers, and transactional exactly-once applies only when the whole pipeline stays inside Kafka (",{"tag":191,"href":64,"children":309},[310],"Confluent","). An ERP call is outside that boundary, so exactly-once does not survive it.",{"type":134,"content":313},[314],"That shapes the trade-off between a simple retry loop and a message bus. For one shop and one ERP with modest volume, a synchronous API call from a worker that reads an outbox table is enough. A bus pays off when several consumers need the same order events, for example a warehouse system and analytics, or when the ERP pushes its own business events; S\u002F4HANA, for instance, can publish sales order events to SAP Event Mesh. Either way, the consumer must tolerate duplicates.",{"type":134,"content":316},[317],"Design the retry policy per error class rather than per call. Timeouts and 5xx answers get a bounded number of retries with exponential backoff and jitter; validation errors such as an unknown customer or a blocked material go straight to a person, because repeating them only adds noise. After the last attempt, a message goes to a dead-letter store with its error, so it can be fixed and replayed through the same writer instead of being re-entered by hand.",{"type":151,"level":152,"id":121,"text":122},{"type":134,"content":320},[321,322,197],"The outbox makes sure no order is saved without its message, and idempotency makes sure a message delivered twice does not create two orders. In the transactional outbox pattern, the business write and the outbox row commit in one database transaction, and a relay publishes later; the relay may publish a message more than once, so consumers must be idempotent, for example by tracking processed message ids (",{"tag":191,"href":34,"children":323},[324],"microservices.io",{"type":157,"src":326,"alt":327,"wide":160,"caption":328},"\u002Fimages\u002Fblog\u002Fb2b-order-sync-erp\u002Fdiagram-23.webp","Outbox, idempotent writer and reconciler The shop database commits the order and an outbox row in one transaction. A relay publishes the message and may publish it twice. The ERP writer creates the sales order with an idempotency key, and the ERP dedupes on the shop’s order reference. A reconciler compares shop and ERP states on a schedule and sends repair commands back through the same ERP writer.","Outbox plus idempotent writer plus reconciler: duplicates are expected and absorbed, and repairs use the same write path as normal orders.",{"type":282,"code":330},"-- One transaction: the order and the message that announces it\nBEGIN;\nINSERT INTO orders (id, customer_id, total, status)\n  VALUES ('SO-2026-104233', 'C-40017', 1840.00, 'placed');\nINSERT INTO outbox (id, aggregate_id, type, payload)\n  VALUES (gen_random_uuid(), 'SO-2026-104233', 'OrderPlaced', '{\"lines\": 7}');\nCOMMIT;",{"type":282,"code":332},"\u002F\u002F Idempotent consumer (sketch): the shop order id is the dedupe key all the way into the ERP\nasync function handleOrderPlaced(msg: OutboxMessage) {\n  if (await processed.has(msg.id)) return                    \u002F\u002F the relay may deliver twice\n  await withLock(`order:${msg.aggregateId}`, async () => {    \u002F\u002F one writer per order at a time\n    const existing = await erp.findSalesOrder({ customerReference: msg.aggregateId })\n    if (!existing) await erp.createSalesOrder(toErpPayload(msg)) \u002F\u002F header and items in one call\n  })\n  await processed.add(msg.id)\n}",{"type":134,"content":334},[335,336,338,339,342],"Use the shop’s order id as the key all the way through, and put it into a field the ERP can search, such as the customer purchase order number on an SAP sales order or ",{"tag":282,"children":337},[288]," in Business Central. For HTTP APIs you control, Stripe’s idempotency design is a good reference: the client sends a key, retries get the saved response, parameters are compared with the original request, and keys can be pruned after at least 24 hours (",{"tag":191,"href":61,"children":340},[341],"Stripe","). Find-then-create has a race, which is why the sketch above holds a lock per order; a unique constraint on the reference in the ERP, where available, is stronger.",{"type":344,"variant":345,"title":346,"body":347},"callout","warn","Never delete to correct",[348],[349],"When an order went wrong, do not delete it in either system to “resync”. Cancel it with a compensating document and create a new one through the same writer. Deletes break the audit trail, confuse reconciliation and, in an ERP, can collide with deliveries or invoices that already reference the order.",{"type":151,"level":152,"id":124,"text":125},{"type":134,"content":352},[353],"Duplicate sales orders, lost acknowledgments, lost orders, partial failures such as a header without items, price or unit drift, and status drift after a restore or a manual edit in the ERP. Reconciliation is the routine job that finds them, not an emergency tool.",{"type":134,"content":355},[356,357,360],"Mercari’s engineering team describes three practical buckets for payment reconciliation that transfer directly to orders: success locally but not in the dependent system, success in the dependent system but not locally, and success in both with mismatched details, which usually needs case-by-case handling (",{"tag":191,"href":67,"children":358},[359],"Mercari Engineering","). I would keep three kinds of state per order, what the shop wants, what it knows was applied with a last-reconciled timestamp, and what the ERP says, and let the reconciler detect, classify, record and queue a repair command. The repair itself runs through the same canonical writer as normal orders, so all the idempotency rules still apply. Track the age of the oldest unreconciled order as a metric; when it grows, something upstream has stopped.",{"type":134,"content":362},[363,364,368,369,149],"When failures do pile up, someone has to read IDoc status records, OData errors and dead-letter queues and decide what to retry; an agent can help there, as described in ",{"tag":145,"to":365,"children":366},"\u002Fblog\u002Ferp-integration-error-agent",[367],"an agent that triages ERP integration errors",". For orders that arrive by email rather than through the shop, the same idempotency rules apply at the create call, see ",{"tag":145,"to":370,"children":371},"\u002Fblog\u002Fb2b-email-order-entry-llm",[372],"email order entry automation",{"type":151,"level":152,"id":127,"text":128},{"type":134,"content":375},[376,377,197],"Test against an ERP sandbox or QA client with test company codes, and make a stuck order traceable from the shop commit to the ERP acknowledgment. OpenTelemetry’s messaging conventions define publish, receive, process and settle spans with links between producer and consumer traces, which is exactly the shape of this flow (",{"tag":191,"href":70,"children":378},[379],"OpenTelemetry",{"type":182,"ordered":160,"items":381},[382,384,386,388,390,392,394,396],[383],"Write field ownership down: which system owns which order field and status.",[385],"Commit the order and the outbox row in one transaction.",[387],"Carry the shop order id into a searchable ERP field and dedupe on it.",[389],"Commit explicitly where the API requires it, such as BAPI_TRANSACTION_COMMIT.",[391],"Send the same order twice in the sandbox and assert exactly one sales order; I script this from the real shop UI with Playwright MCP.",[393],"Run a scheduled reconciliation in both directions and record every finding.",[395],"Trace each order across publish, receive and process, and alert on queue age and oldest unreconciled order.",[397],"Correct with compensating documents, never with deletes.",{"type":134,"content":399},[400,401,405,406,149],"If you are connecting a B2B shop to SAP, Business Central or Infor and want the order loop to stay boring, see my ",{"tag":145,"to":402,"children":403},"\u002Fexpertise\u002Fb2b-ecommerce-developer",[404],"B2B e-commerce work",". Configured items add their own payload rules on top; see ",{"tag":145,"to":407,"children":408},"\u002Fblog\u002Fheadless-product-configurator-b2b",[409],"headless product configurators for B2B",{"type":151,"level":152,"id":130,"text":131},{"type":182,"ordered":160,"items":412},[413,416,419,422,425,428,431,434,437,440,443,446],[414],{"tag":191,"href":38,"children":415},[37],[417],{"tag":191,"href":41,"children":418},[40],[420],{"tag":191,"href":44,"children":421},[43],[423],{"tag":191,"href":47,"children":424},[46],[426],{"tag":191,"href":50,"children":427},[49],[429],{"tag":191,"href":53,"children":430},[52],[432],{"tag":191,"href":56,"children":433},[55],[435],{"tag":191,"href":34,"children":436},[58],[438],{"tag":191,"href":61,"children":439},[60],[441],{"tag":191,"href":64,"children":442},[63],[444],{"tag":191,"href":67,"children":445},[66],[447],{"tag":191,"href":70,"children":448},[69],[450,524,580,654],{"slug":451,"published":452,"minutes":453,"category":7,"tags":454,"keywords":460,"about":472,"sources":481,"cover":518,"og":519,"expertise":73,"locales":520,"lang":75,"title":521,"description":522,"coverAlt":523},"schema-org-structured-data-b2b-shop","2026-10-17",10,[455,456,457,458,459],"Schema.org","Structured data","B2B e-commerce","Technical SEO","Nuxt",[461,462,463,464,465,466,467,468,469,470,471],"schema.org b2b shop","product structured data b2b","merchant listing structured data","product snippet vs merchant listing","unitpricespecification referencequantity","productgroup variants structured data","organization schema vatid","b2b prices behind login seo","json-ld nuxt schema org","merchant return policy structured data","mpn gtin structured data",[473,475,478],{"name":455,"url":474},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSchema.org",{"name":476,"url":477},"JSON-LD","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON-LD",{"name":479,"url":480},"Business-to-business","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBusiness-to-business",[482,485,488,491,494,497,500,503,506,509,512,515],{"title":483,"url":484},"Google Search Central: Introduction to Product structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct",{"title":486,"url":487},"Google Search Central: Merchant listing (Product, Offer) structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fmerchant-listing",{"title":489,"url":490},"Google Search Central: Product snippet structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-snippet",{"title":492,"url":493},"Google Search Central: Product variant structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-variants",{"title":495,"url":496},"Google Search Central: Organization structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Forganization",{"title":498,"url":499},"Google Search Central: Merchant return policy structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Freturn-policy",{"title":501,"url":502},"Google Search Central: General structured data guidelines","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fsd-policies",{"title":504,"url":505},"Google Search Central: Generate structured data with JavaScript","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fgenerate-structured-data-with-javascript",{"title":507,"url":508},"Schema.org: Offer","https:\u002F\u002Fschema.org\u002FOffer",{"title":510,"url":511},"Schema.org: UnitPriceSpecification","https:\u002F\u002Fschema.org\u002FUnitPriceSpecification",{"title":513,"url":514},"Unhead: Product schema (defineProduct)","https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fschema-org\u002Fapi\u002Fschema\u002Fproduct",{"title":516,"url":517},"Nuxt SEO: Schema.org identity setup","https:\u002F\u002Fnuxtseo.com\u002Fdocs\u002Fschema-org\u002Fguides\u002Fsetup-identity","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fog.jpg",[75,76,77],"Schema.org structured data for B2B shops: prices behind logins, units and variants","Product markup for B2B shops: snippet or merchant listing, which price to publish, part numbers, unit pricing, variants, Organization and SSR in Nuxt.","A product rectangle with a framed outline and a small price slot hidden behind a low wall.",{"slug":525,"published":452,"minutes":526,"category":7,"tags":527,"keywords":532,"about":544,"sources":552,"cover":574,"og":575,"expertise":73,"locales":576,"lang":75,"title":577,"description":578,"coverAlt":579},"ai-assistant-b2b-shop-customer-support",9,[457,528,529,530,531],"AI assistant","Customer support","LLM agents","EU AI Act",[533,534,535,536,537,538,539,540,541,542,543],"ai assistant b2b shop","ai customer support b2b ecommerce","llm chatbot order status","ai agent customer service tools","chatbot liability air canada","klarna ai assistant results","human handoff chatbot design","eu ai act chatbot disclosure","owasp excessive agency","ai support resolution rate","b2b customer portal chatbot",[545,548,551],{"name":546,"url":547},"Customer service","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_service",{"name":549,"url":550},"Chatbot","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChatbot",{"name":479,"url":480},[553,556,559,562,565,568,571],{"title":554,"url":555},"Klarna: AI assistant handles two-thirds of customer service chats in its first month (press release, 2024)","https:\u002F\u002Fwww.klarna.com\u002Finternational\u002Fpress\u002Fklarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month\u002F",{"title":557,"url":558},"Entrepreneur: Klarna is hiring customer service agents again, according to its CEO (May 2025)","https:\u002F\u002Fwww.entrepreneur.com\u002Fbusiness-news\u002Fklarna-ceo-reverses-course-by-hiring-more-humans-not-ai\u002F491396",{"title":560,"url":561},"ABA Business Law Today: BC tribunal confirms companies remain liable for information provided by AI chatbot","https:\u002F\u002Fwww.americanbar.org\u002Fgroups\u002Fbusiness_law\u002Fresources\u002Fbusiness-law-today\u002F2024-february\u002Fbc-tribunal-confirms-companies-remain-liable-information-provided-ai-chatbot\u002F",{"title":563,"url":564},"OWASP GenAI Security Project: LLM06:2025 Excessive Agency","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm062025-excessive-agency\u002F",{"title":566,"url":567},"OWASP GenAI Security Project: LLM05:2025 Improper Output Handling","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm052025-improper-output-handling\u002F",{"title":569,"url":570},"EU AI Act, Article 50: Transparency obligations","https:\u002F\u002Fartificialintelligenceact.eu\u002Farticle\u002F50\u002F",{"title":572,"url":573},"Intercom: Fin pricing and billable outcomes","https:\u002F\u002Ffin.ai\u002Fpricing","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fog.jpg",[75,76,77],"An AI assistant for B2B shop support: tools, permissions and the human handoff","How to build an AI support assistant for a B2B shop: which requests to automate, tools that run in the customer context, handoff, AI Act and metrics.","A tall wall with one narrow gap; an arrow passes through it into a boxed enclosure of three squares, and another leads down to a tray.",{"slug":581,"published":452,"minutes":582,"category":7,"tags":583,"keywords":588,"about":600,"sources":610,"cover":647,"og":648,"expertise":649,"locales":650,"lang":75,"title":651,"description":652,"coverAlt":653},"vue-ai-chat-ui-streaming-accessibility",11,[584,459,585,586,587],"Vue","Accessibility","Streaming","AI chat",[589,590,591,592,593,594,595,596,597,598,599],"vue ai chat ui","nuxt llm streaming","sse vs fetch streaming","aria-live streaming text","accessible chat interface","abort fetch stream vue","dompurify markdown llm","role log chat","reduced motion chat ui","wcag 2.2 chat","screen reader streaming",[601,604,607],{"name":602,"url":603},"Web accessibility","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWeb_accessibility",{"name":605,"url":606},"Server-sent events","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-sent_events",{"name":608,"url":609},"Vue.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FVue.js",[611,614,617,620,623,626,629,632,635,638,641,644],{"title":612,"url":613},"MDN: Using server-sent events","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events\u002FUsing_server-sent_events",{"title":615,"url":616},"MDN: AbortController","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FAbortController",{"title":618,"url":619},"MDN: ReadableStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FReadableStream",{"title":621,"url":622},"MDN: TextDecoderStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextDecoderStream",{"title":624,"url":625},"MDN: ARIA log role","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAccessibility\u002FARIA\u002FReference\u002FRoles\u002Flog_role",{"title":627,"url":628},"W3C: Understanding SC 4.1.3 Status Messages","https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG22\u002FUnderstanding\u002Fstatus-messages.html",{"title":630,"url":631},"W3C WAI: What's new in WCAG 2.2","https:\u002F\u002Fwww.w3.org\u002FWAI\u002Fstandards-guidelines\u002Fwcag\u002Fnew-in-22\u002F",{"title":633,"url":634},"MDN: prefers-reduced-motion","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-reduced-motion",{"title":636,"url":637},"DOMPurify README","https:\u002F\u002Fgithub.com\u002Fcure53\u002FDOMPurify",{"title":639,"url":640},"markdown-it README","https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",{"title":642,"url":643},"AI SDK docs: useChat","https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-ui\u002Fchatbot",{"title":645,"url":646},"Vue docs: nextTick","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fgeneral.html#nexttick","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fog.jpg","vue-nuxt-developer",[75,76,77],"Streaming AI chat in Vue and Nuxt: accessible by design","How to build a Vue or Nuxt AI chat UI: fetch streams, abort and retry, safe markdown rendering, aria-live announcements, keyboard support and reduced motion.","A message card with a careful caret, a dotted line opening into ripples and a check.",{"slug":655,"published":452,"minutes":526,"category":7,"tags":656,"keywords":661,"about":673,"sources":683,"cover":716,"og":717,"expertise":649,"locales":718,"lang":75,"title":719,"description":720,"coverAlt":721},"llm-translation-pipeline-i18n",[657,658,659,584,660],"Translation","i18n","LLM","Localization",[662,663,664,665,666,667,668,669,670,671,672],"llm translation pipeline","icu messageformat placeholders","vue i18n llm translation","translation memory llm","glossary enforcement translation","german informal sie du","hungarian translation quality llm","structured output json translation","machine translation post-editing","locale file validation","cldr plural categories",[674,677,680],{"name":675,"url":676},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",{"name":678,"url":679},"ICU MessageFormat","https:\u002F\u002Funicode-org.github.io\u002Ficu\u002Fuserguide\u002Fformat_parse\u002Fmessages\u002F",{"name":681,"url":682},"Vue I18n","https:\u002F\u002Fvue-i18n.intlify.dev\u002F",[684,686,689,692,695,698,701,704,707,710,713],{"title":685,"url":679},"Unicode ICU User Guide: Formatting Messages",{"title":687,"url":688},"Vue I18n: Message Format Syntax","https:\u002F\u002Fvue-i18n.intlify.dev\u002Fguide\u002Fessentials\u002Fsyntax",{"title":690,"url":691},"Nuxt i18n: Getting started","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fgetting-started",{"title":693,"url":694},"Unicode CLDR: Plural rules","https:\u002F\u002Fcldr.unicode.org\u002Findex\u002Fcldr-spec\u002Fplural-rules",{"title":696,"url":697},"OpenAI: Structured outputs","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",{"title":699,"url":700},"DeepL API: Glossaries","https:\u002F\u002Fdevelopers.deepl.com\u002Fdocs\u002Fapi-reference\u002Fglossaries",{"title":702,"url":703},"W3C: Localization vs. internationalization","https:\u002F\u002Fwww.w3.org\u002FInternational\u002Fquestions\u002Fqa-i18n",{"title":705,"url":706},"GNU gettext manual","https:\u002F\u002Fwww.gnu.org\u002Fsoftware\u002Fgettext\u002Fmanual\u002Fgettext.html",{"title":708,"url":709},"RFC 5646: Tags for Identifying Languages","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc5646",{"title":711,"url":712},"Rat für deutsche Rechtschreibung","https:\u002F\u002Fwww.rechtschreibrat.com\u002F",{"title":714,"url":715},"Microsoft Writing Style Guide","https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fstyle-guide\u002Fwelcome\u002F","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fog.jpg",[75,76,77],"An LLM translation pipeline for i18n: glossary, placeholders and review","How to translate product and website strings with LLMs without breaking placeholders, plurals or tone: glossary, ICU messages, QA checks and review gates.","Three source cards, one translated card with a glossary ribbon, then a checked clipboard.",1792337027531]