Online order intake
A customer checks out on the company webshop. The moment they confirm, the order has to land in the back-office order database so the business can act on it within seconds. The webshop is one party, the order database is the other, and the job is to carry every order from the first to the second without losing any of them. The end result is one new row in the orders table for every checkout. If a save ever fails, the order is not quietly dropped: someone in operations gets an email so it can be fixed and tried again.
So the situation is simple to picture. The webshop hands over each new order, the order database needs to store it, and nothing should fall through the gap in between. The one thing to watch is a failed save, which has to raise a flag rather than disappear.
Here is how iQBus builds that. Messages move across a shared backbone called the bus, and every order travels through it as a canonical Order, one agreed shape the rest of the business reads. The way in is a receive port (the entry point that takes a message in), bound to the API Listener (the adapter, the connector to a particular kind of system, here an HTTP endpoint the webshop posts to). The storefront posts its order and gets an acknowledgement as soon as the message is on the bus; the database write happens just after, on the bus's own time.
The order arrives in the webshop's own shape and is typed WebOrder straight away: the port’s Adapter Message Type is applied the instant the Listener hands the body on, so there is never a moment where the message has no type. A map (a rule that rewrites one shape into another) then translates it into the canonical Order message type before it is published, so the storefront's field names stop at the edge, map at the edges.
The way out is a send port (a port that delivers a message to an outside system), bound to the SQL Caller and configured as a stored procedure call. Rather than being told what to deliver, the send port states which messages it wants, a subscription. Its subscription is a single predicate:
{{Message.MessageType}} == "Order"
and its parameter bindings draw on three promotions defined on Order:
| Parameter | Value |
|---|---|
| OrderNumber | {{Promoted.Order.OrderNumber}} |
| CustomerId | {{Promoted.Order.CustomerId}} |
| OrderTotal | {{Promoted.Order.OrderTotal}} |
When it fails. If the insert exhausts its retries, the run is recorded as failed in tracking, order payload intact, and an Activity Notification (a per-Application email subscription that fires on failed runs, On Error Only) emails operations. The order stays where it is, visible for replay once the cause is fixed, fail loud, never drop. A production integration would go one step further and route the failed order on purpose through an Exception Message Type; Step 5 explains the trade-off.
Build it, step by step. The steps follow the integration itself, from the webshop to the database, not the order that is quickest to click through. Every Authentication, message type and map is created from the dialog that first asks for it, so you never have to leave the port you are configuring to go and make something it needs.
| Setting | Value |
|---|---|
| Name | AcmeOrders |
| Namespace | AcmeOrders, the value the App header carries in Step 1 |
| Tracking | Enabled + Body |
| Description | Webshop orders into the order database |
The integration begins where the order does, at the endpoint the webshop posts to. Under the application’s Ports, create a receive port bound to the API Listener. Nothing it needs exists yet, and that is fine: each field below creates what it references on the spot. If you have to step away mid-setup, Save Draft keeps the port under just its name; a draft does not exist to the runtime until the required fields are filled in and the port is saved properly.
| Setting | Value |
|---|---|
| Name | WebshopOrders |
| Application | AcmeOrders (preset) |
| Port Type | Receive |
| Adapter | API Listener |
| Adapter Message Type | WebOrder, created below, at this field |
| Exception Message Type | empty; a rejected or malformed request is answered to the caller, the failure this flow handles is the insert, on the send port |
| Way | One |
Open the adapter configuration (the button beside Adapter). The first thing it asks for is the credential every caller must present. Nothing is there yet, so create it right here at the Auth Config field; the dialog opens on top of the port and returns you to it when you save:
| Setting | Value |
|---|---|
| Name | StorefrontToken |
| Adapter | API Listener (preset) |
| Definition | API Listener Token |
| Token (Partner Config) | acme-webshop-7f3a9c2e4b1d8e6f |
The token is a shared secret: whatever you type here is what the webshop (and in Step 6, Postman) has to send on every call. The example above is fine for the tutorial; for a real storefront generate a long random value and hand it to the webshop team through a secrets channel, never by email.
Back in the adapter configuration, finish the rest:
| Setting | Value |
|---|---|
| Auth Config Type | Static |
| Auth Config | StorefrontToken, just created |
| Verb | POST |
| Headers | the two rows below |
| Url | the shared ingress, ending in a segment you choose: webshop-orders |
| Header | Value | Why |
|---|---|---|
| App | AcmeOrders | the Application’s namespace, narrows the request to this Application |
| Purpose | OrderIntake | keeps this Listener distinct if the App later gains a second one (e.g. a stock lookup) |
Back on the port’s General tab, one row of the table above is still open, the required Adapter Message Type. The runtime applies it the instant the Listener releases the body, so the message is typed before anything else runs. The storefront’s shape has no type yet, so create it here at the field. A message type is a name plus a Format, no schema required, and it can hold a sample of the payload. Paste the sample below into Sample data: it has no runtime effect, but it documents the shape for whoever opens the type later, and it is the same payload you will post in Step 6.
{
"order_no": "10472",
"ship_region": "EMEA",
"buyer": { "ref": "C-2201", "company": "Nordwind Retail" },
"account_mgr": "j.driessen@example.com",
"grand_total": 1249.50
}| Setting | Value |
|---|---|
| Name | WebOrder |
| Format | JSON |
| Sample data | the WebOrder JSON above |
| Promotions | none, nothing routes on the raw shape |
Saving the type returns you to the port with Adapter Message Type now reading WebOrder, which completes the General table above. Leave the port open; the next step continues on its Map step.
Still inside WebshopOrders, go to the Map step and set Map Assignment to Typed. Add one row and, at its Map field, create the map. A map is not a file, it is a named resource defined by its name and the two message types it sits between:
| Setting | Value |
|---|---|
| Name | WebOrderToOrder |
| Source message type | WebOrder, from Step 1 |
| Target message type | Order, created next, at this field |
The target does not exist yet either, so create it from the Target field. This is the shape the rest of the business reads, and the three values the SQL send port binds in Step 4 are lifted out of it by promotions. A promotion is a named path into the payload (JPath for a JSON type), so before naming any, look at the payload the map will produce:
{
"orderNumber": "SO-10472",
"channel": "webshop",
"region": "EMEA",
"customer": { "id": "C-2201", "name": "Nordwind Retail" },
"accountManager": { "email": "j.driessen@example.com" },
"total": 1249.50,
"currency": "EUR"
}Paste that JSON into the type’s Sample data first. Then, for each promotion, add a row, open its ellipsis menu and choose Generate Path: it shows the sample, you click the node you want, and the JPath is written for you. Click orderNumber and you get $.orderNumber; click the id inside customer and you get $.customer.id. You can also type the paths by hand, but the tool removes the guesswork on deeper shapes.
| Setting | Value |
|---|---|
| Name | Order |
| Format | JSON |
| Sample data | the Order JSON above |
| Promotion | Path | Read by |
|---|---|---|
| OrderNumber | $.orderNumber | the @OrderNumber parameter, Step 4 |
| CustomerId | $.customer.id | the @CustomerId parameter, Step 4 |
| OrderTotal | $.total | the @OrderTotal parameter, Step 4 |
Adapter parameters are plain strings: they bind {{…}} tokens such as {{Promoted.Order.OrderNumber}} but never evaluate a body path themselves, which is why the promotions have to exist on the type. Save the type; you are back in the map with both types selected.
Now the map body. You do not have to write it by hand. Open the AI Accelerator, describe the translation in plain language, and let it generate the XSLT for you to review and paste into the map. It knows how iQBus maps read JSON and how to line fields up by meaning, but it sees nothing of your Application: not the message types you just created, not the promotions, nothing but the text you give it. So the prompt carries everything the map needs: the WebOrder sample, the Order sample, and the paths the promotions read. A prompt like this is enough:
Build a map from a JSON message type named WebOrder to a JSON message type named Order.
Match the storefront fields to the canonical order by meaning: the order number, the shipping region, the buyer's reference and company name as the customer's id and name, the account manager's email address, and the grand total as a number. Prefix the order number with SO- so it reads SO-10472. Add two constant fields, channel set to webshop and currency set to EUR. Three paths must be exactly $.orderNumber, $.customer.id and $.total, because promotions read them. Output indented JSON.
The WebOrder source looks like this (yours will carry different values but the same shape):
{
"order_no": "10472",
"ship_region": "EMEA",
"buyer": { "ref": "C-2201", "company": "Nordwind Retail" },
"account_mgr": "j.driessen@example.com",
"grand_total": 1249.50
}
The Order target must look like this:
{
"orderNumber": "SO-10472",
"channel": "webshop",
"region": "EMEA",
"customer": { "id": "C-2201", "name": "Nordwind Retail" },
"accountManager": { "email": "j.driessen@example.com" },
"total": 1249.50,
"currency": "EUR"
}The AI Accelerator writes a first draft, not a finished map, so test it before you rely on it. The map editor has a tester built in: paste the sample WebOrder as the input, run the map, and compare the result with the Order sample above. What has to match exactly is the three promoted paths, $.orderNumber, $.customer.id and $.total; field order and formatting may differ, because the Accelerator does not return the exact same result twice. If you would rather start from working code, expand it here to read or copy.
Show the full map code (XSLT 3.0), expand to read or copy
Maps are authored in XSLT 3.0 and run on Saxon HE 12.9. The WebOrder JSON arrives as a string, so the template matches / and parses it with parse-json(); from there fields are read with the ?key accessor, the result is built as a map{} and serialized back to JSON. The account manager’s address rides along for the order fan-out notification.
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <!-- The WebOrder JSON arrives as a string; parse-json() turns it into an XPath 3.1 map --> <xsl:template match="/"> <xsl:variable name="in" select="parse-json(.)"/> <xsl:value-of select="serialize( map { 'orderNumber': concat('SO-', string($in?order_no)), 'channel': 'webshop', 'region': string($in?ship_region), 'customer': map { 'id': string($in?buyer?ref), 'name': string($in?buyer?company) }, 'accountManager': map { 'email': string($in?account_mgr) }, 'total': number($in?grand_total), 'currency': 'EUR' }, map { 'method': 'json', 'indent': true() } )"/> </xsl:template> </xsl:stylesheet>
Save the map, and the row on the port’s Map step reads:
| Source Message Type | Map | Target Message Type |
|---|---|---|
| WebOrder | WebOrderToOrder | Order |
Save the port. WebshopOrders is complete, stopped, and waiting; it goes live in Step 6.
The order database is the system you are integrating with, not part of iQBus, so it is set up by hand rather than asked of the AI Accelerator. Create the table and the procedure the send port calls in Step 4. The order number is the primary key, so the MERGE is idempotent: a replayed order updates instead of duplicating.
CREATE TABLE dbo.Orders ( OrderNumber VARCHAR(40) NOT NULL PRIMARY KEY, CustomerId VARCHAR(40) NOT NULL, CustomerName NVARCHAR(200) NULL, Region VARCHAR(20) NULL, OrderTotal DECIMAL(18,2) NOT NULL, Currency CHAR(3) NOT NULL CONSTRAINT DF_Orders_Cur DEFAULT 'EUR', ReceivedAt DATETIME2 NOT NULL CONSTRAINT DF_Orders_Rcv DEFAULT SYSUTCDATETIME() );
CREATE OR ALTER PROCEDURE dbo.UpsertOrder @OrderNumber VARCHAR(40), @CustomerId VARCHAR(40), @OrderTotal DECIMAL(18,2) AS BEGIN SET NOCOUNT ON; MERGE dbo.Orders AS tgt USING (SELECT @OrderNumber AS OrderNumber) AS src ON tgt.OrderNumber = src.OrderNumber WHEN MATCHED THEN UPDATE SET CustomerId = @CustomerId, OrderTotal = @OrderTotal WHEN NOT MATCHED THEN INSERT (OrderNumber, CustomerId, OrderTotal) VALUES (@OrderNumber, @CustomerId, @OrderTotal); END;
Under the application’s Ports, create a send port bound to the SQL Caller. Rather than being told what to deliver, a send port states which messages it wants; this one subscribes to every canonical order the receive port publishes.
| Setting | Value |
|---|---|
| Name | OrderToDatabase |
| Application | AcmeOrders (preset) |
| Port Type | Send |
| Adapter | SQL Caller |
| Adapter Message Type | none; a one-way send port publishes nothing back |
| Exception Message Type | empty; this first tutorial relies on the Activity Notification in Step 5, which says why |
| Way | One |
| Subscription | {{Message.MessageType}} == "Order" |
Open the adapter configuration. At the Auth Config field, create the database credential in place, exactly as you did for the storefront token:
| Setting | Value |
|---|---|
| Name | OrderDbSql |
| Adapter | SQL Caller (preset) |
| Definition | SQL Server Connection |
| Connection String (Database Config) | one of the two below |
The SQL Caller connects from where iQBus runs, so two things decide the string: how iQBus reaches the server and how the server identifies iQBus. Reachability first: an Azure SQL database on a public endpoint needs iQBus’s outbound address allowed through the server firewall; one behind a private endpoint has to sit in, or be peered with, the iQBus network; an on-premises SQL Server is reached over a VPN or ExpressRoute link, and its hostname has to resolve from the iQBus side. Then the identity, which is what the two strings differ in. iQBus supports SQL authentication and managed identity; it does not support Entra ID interactive sign-in, and putting an Entra account’s username and password into a connection string is strongly discouraged, so neither appears here. Replace the placeholders in angle brackets with your own values:
| Method | Connection String | Use it when |
|---|---|---|
| SQL authentication | Server=tcp:<server>.database.windows.net,1433;Database=<database>;User ID=<login>;Password=<password>;Encrypt=True;TrustServerCertificate=False; | the simplest to start with, and the only option for a SQL Server that is not Entra-integrated; the password lives in this Authentication, encrypted at rest |
| Managed identity | Server=tcp:<server>.database.windows.net,1433;Database=<database>;Authentication=Active Directory Managed Identity;Encrypt=True; | no secret at all: the iQBus deployment’s own Azure identity is granted access on the database (CREATE USER [<identity>] FROM EXTERNAL PROVIDER, then the roles it needs); the strongest choice for production |
For an on-premises server, swap the Server= value for its hostname or IP and port and use SQL authentication. For this tutorial, SQL authentication against a database whose firewall lets iQBus in is enough. Whichever you pick, the login needs EXECUTE on dbo.UpsertOrder.
Back in the adapter configuration, set the command and its parameters together. The procedure from Step 3 takes three values, one key/value row each, bound to the promotions you defined on Order in Step 2:
| Setting | Value |
|---|---|
| Auth Config | OrderDbSql, just created |
| Command Type | Stored Procedure |
| Command Text | dbo.UpsertOrder, from Step 3 |
| Parameter | Value |
|---|---|
| OrderNumber | {{Promoted.Order.OrderNumber}} |
| CustomerId | {{Promoted.Order.CustomerId}} |
| OrderTotal | {{Promoted.Order.OrderTotal}} |
Save the port.
The storefront already has its acknowledgement by the time the insert runs, so a database failure is an internal recovery task, not a lost order: the run is recorded as failed in tracking with the order payload intact. Someone still has to hear about it. For this first tutorial the simplest way is an Activity Notification, a per-Application email subscription that fires on failed runs. Under the application’s Activity Notifications, create it:
| Setting | Value |
|---|---|
| Name | OrderIntakeErrors |
| Application | AcmeOrders (preset) |
| On Error Only | On |
| Subscription | All |
| Mode | On Event |
| Recipients | one row: Name Operations, Email ops@acme.example |
| Is Enabled | Enabled |
There is nothing to deploy, everything you configured is already saved and live, because iQBus applies changes immediately. To bring the flow online you start the send port first, then the receive port: start OrderToDatabase, then start WebshopOrders. Starting the subscriber before the producer means the Listener never accepts an order before the database send port is ready to receive it. (To take the flow offline, stop the receive port first, for the same reason.) Tracking is already Enabled + Body from the Application, so every run you make now is recorded step by step with its payload.
We use Postman to stand in for the webshop; any similar HTTP client (Insomnia, Bruno, curl) works the same way. Install it, sign in or skip the account, and create a new request (New → HTTP). Fill it in from the values you set in Step 1:
| Request part | Value |
|---|---|
| Method | POST |
| URL | the port’s Url, the one ending in webshop-orders; open WebshopOrders and copy it from the adapter configuration |
| Header App | AcmeOrders |
| Header Purpose | OrderIntake |
| Header Authorization | Bearer acme-webshop-7f3a9c2e4b1d8e6f, the StorefrontToken value |
| Header Content-Type | application/json |
| Body | raw, type JSON, the WebOrder sample from Step 1 pasted verbatim |
Headers go on the Headers tab, one row each; the body on the Body tab with raw selected and JSON picked in the dropdown beside it. Save the request as Acme webshop order so you can send it again later.
Click Send. A success status comes back as soon as the message is on the bus; the acknowledgement means iQBus has the order, not that the row is written yet, that happens a moment later on the bus’s own time. Then open Tracking, find the run, and step through it, opening each message body as you go:
| Step in Tracking | What to confirm |
|---|---|
| API Listener receives | the raw JSON arrives and is typed WebOrder at the adapter handoff |
| Map WebOrderToOrder | the body is now the canonical Order, with SO-10472 as the order number |
| Publish | the Order is on the bus and OrderToDatabase picked it up |
| SQL Caller | the call to dbo.UpsertOrder succeeded with the three promoted values |
Finally check the database itself: SELECT * FROM dbo.Orders WHERE OrderNumber = 'SO-10472' returns one row. Send the same request again and the row count stays at one, that is the MERGE doing its job.
Stop the database, or change the request’s order_no to a value longer than 40 characters so the insert violates the column, and send again. The call still succeeds, the order is on the bus, but the run fails at the SQL Caller after its retries. Confirm the operations mailbox gets the OrderIntakeErrors email from Step 5, and that the failed run sits in tracking with the order payload intact, available for replay once the database is back.