Skip to content
iQBus HomeDocumentationBlogContact
Updated September 7, 2026
Tutorials/Getting started/Online order intake

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.

Every checkout becomes one saved order Webshop checkout customer confirms order Save the order saved save failed Order database one row added for this order Email to operations held to fix and retry, never lost

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:

EXPRESSION
{{Message.MessageType}} == "Order"

and its parameter bindings draw on three promotions defined on Order:

ParameterValue
OrderNumber{{Promoted.Order.OrderNumber}}
CustomerId{{Promoted.Order.CustomerId}}
OrderTotal{{Promoted.Order.OrderTotal}}
Webshop POST API Listener map WebOrder → Order BUS SQL Caller OrderToDatabase Orders DB run failed Activity Notification emails operations

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.

Make the insert idempotent. A retry after a timeout can run the procedure twice. Key the insert on the order number, MERGE or insert-if-absent, so a replayed order updates rather than duplicates. See idempotent by design.

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.

Before you start. Everything in this walkthrough lives inside one Application. Under Applications, create it and select it, so every port, map, and message type below is created in its scope. The Application is also where tracking is set, and one choice covers every port in it: while you are building, use Enabled + Body, which records each run step by step with the message body, so you can open any order in tracking and see exactly what was mapped and inserted. Once the flow is proven and moves to production, drop it to Only on Error if you wish.
Application
SettingValue
NameAcmeOrders
NamespaceAcmeOrders, the value the App header carries in Step 1
TrackingEnabled + Body
DescriptionWebshop orders into the order database
1
Step One
Create the receive port

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.

Receive port
SettingValue
NameWebshopOrders
ApplicationAcmeOrders (preset)
Port TypeReceive
AdapterAPI Listener
Adapter Message TypeWebOrder, created below, at this field
Exception Message Typeempty; a rejected or malformed request is answered to the caller, the failure this flow handles is the insert, on the send port
WayOne
Configure the adapter

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:

Authentication, storefront credential
SettingValue
NameStorefrontToken
AdapterAPI Listener (preset)
DefinitionAPI 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:

Adapter configuration
SettingValue
Auth Config TypeStatic
Auth ConfigStorefrontToken, just created
VerbPOST
Headersthe two rows below
Urlthe shared ingress, ending in a segment you choose: webshop-orders
Headers
HeaderValueWhy
AppAcmeOrdersthe Application’s namespace, narrows the request to this Application
PurposeOrderIntakekeeps this Listener distinct if the App later gains a second one (e.g. a stock lookup)
The Url must be unique across the whole installation. Every API Listener is hosted under the same base address, and the final segment is yours to set. It has to be unique across the entire iQBus deployment, not just inside this Application; the App header does not make it unique. If the segment is already taken, iQBus tells you when you save, and you pick another. Copy the full Url once it is accepted, Postman needs it in Step 6.
Type the message at the adapter

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.

The WebOrder the storefront posts
JSON
{
  "order_no": "10472",
  "ship_region": "EMEA",
  "buyer": { "ref": "C-2201", "company": "Nordwind Retail" },
  "account_mgr": "j.driessen@example.com",
  "grand_total": 1249.50
}
Message type, the storefront’s shape
SettingValue
NameWebOrder
FormatJSON
Sample datathe WebOrder JSON above
Promotionsnone, 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.


2
Step Two
Build the map, and the Order it produces

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:

Map
SettingValue
NameWebOrderToOrder
Source message typeWebOrder, from Step 1
Target message typeOrder, created next, at this field
The canonical Order

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:

JSON
{
  "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.

Message type, the canonical order
SettingValue
NameOrder
FormatJSON
Sample datathe Order JSON above
Promotions on Order
PromotionPathRead by
OrderNumber$.orderNumberthe @OrderNumber parameter, Step 4
CustomerId$.customer.idthe @CustomerId parameter, Step 4
OrderTotal$.totalthe @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.

Build it with the AI Accelerator

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:

AI ACCELERATOR PROMPT
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.

XSLT 3.0
<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:

Map assignment
Source Message TypeMapTarget Message Type
WebOrderWebOrderToOrderOrder

Save the port. WebshopOrders is complete, stopped, and waiting; it goes live in Step 6.


3
Step Three
Create the order database objects

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.

The orders table
SQL
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()
);
The upsert procedure
SQL
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;

4
Step Four
Create the SQL send port

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.

Send port
SettingValue
NameOrderToDatabase
ApplicationAcmeOrders (preset)
Port TypeSend
AdapterSQL Caller
Adapter Message Typenone; a one-way send port publishes nothing back
Exception Message Typeempty; this first tutorial relies on the Activity Notification in Step 5, which says why
WayOne
Subscription{{Message.MessageType}} == "Order"
Configure the adapter

Open the adapter configuration. At the Auth Config field, create the database credential in place, exactly as you did for the storefront token:

Authentication, order database
SettingValue
NameOrderDbSql
AdapterSQL Caller (preset)
DefinitionSQL Server Connection
Connection String (Database Config)one of the two below
Which connection string

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:

MethodConnection StringUse it when
SQL authenticationServer=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 identityServer=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:

Adapter configuration
SettingValue
Auth ConfigOrderDbSql, just created
Command TypeStored Procedure
Command Textdbo.UpsertOrder, from Step 3
Input Parameters
ParameterValue
OrderNumber{{Promoted.Order.OrderNumber}}
CustomerId{{Promoted.Order.CustomerId}}
OrderTotal{{Promoted.Order.OrderTotal}}

Save the port.


5
Step Five
Alert on failures

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:

Activity Notification
SettingValue
NameOrderIntakeErrors
ApplicationAcmeOrders (preset)
On Error OnlyOn
SubscriptionAll
ModeOn Event
Recipientsone row: Name Operations, Email ops@acme.example
Is EnabledEnabled
One-time prerequisite. Notifications go out through the platform email channel, configured once for the whole installation under Email notification configuration (an Entra app registration with Mail.Send). Until that is done, no Activity Notification sends anything. There is no per-Application mail credential.
Know what you chose. An Activity Notification is a monitoring tool, not part of the integration: it tells a person that a run failed and does nothing else, it does not route the order anywhere or handle the exception. The integration-grade design is to set an Exception Message Type on the send port and route the failed order on purpose, to a queue, a ticket, a mailbox, whatever the business wants, exactly like any other message; that is how the failure gets handled rather than announced. Activity Notifications shine as the safety net underneath: anything that fails without being caught by an exception route still reaches a human, who can then go and add the route. We use the notification alone here to keep the first tutorial short; evaluate each real case on its own.

6
Step Six
Start the ports and test with Postman
Start in dependency order

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.

Set up Postman

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:

Postman request
Request partValue
MethodPOST
URLthe port’s Url, the one ending in webshop-orders; open WebshopOrders and copy it from the adapter configuration
Header AppAcmeOrders
Header PurposeOrderIntake
Header AuthorizationBearer acme-webshop-7f3a9c2e4b1d8e6f, the StorefrontToken value
Header Content-Typeapplication/json
Bodyraw, 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.

Post a test order

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 TrackingWhat to confirm
API Listener receivesthe raw JSON arrives and is typed WebOrder at the adapter handoff
Map WebOrderToOrderthe body is now the canonical Order, with SO-10472 as the order number
Publishthe Order is on the bus and OrderToDatabase picked it up
SQL Callerthe 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.

Break it on purpose

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.

Before production. Once the flow is proven, open the Application and drop tracking to Only on Error if body retention is a concern; failed runs keep their payload for replay either way.