Skip to content
Art2link ESB HomeDocumentationBlogContact
Updated May 5, 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 Art2link ESB 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, classified as WebOrder (the port’s Publish Message Type Fallback labels it, since this endpoint only ever receives one thing). 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 failed O365 Mail operations alert

When it fails. Set the SQL send port’s exception message type to OrderInsertFailed. If the insert exhausts its retries, the in-flight order is re-published under that type, payload intact, instead of vanishing. An O365 Mail send port subscribes to it and alerts operations, and the failed run stays visible in tracking for replay once the cause is fixed, fail loud, never drop.

EXPRESSION
{{Message.MessageType}} == "OrderInsertFailed"
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 run in dependency order: every object is created before the object that selects it.

Before you start. Everything in this walkthrough lives inside one Application. Under Applications, create AcmeOrders with Namespace AcmeOrders and select it, so every port, map, and message type below is created in its scope. That Namespace is the value the App header carries in Step 5.
1
Step One
Create the message types
The three types

Under the application’s Message types, create the three types this flow routes on. A message type is a name plus a format, no schema required:

NameFormatPurpose
WebOrderJSONthe storefront’s payload as it arrives
OrderJSONthe canonical shape the rest of the estate consumes
OrderInsertFailedJSONthe exception type the failure path publishes under (Step 7)

Promotions live on the message type, so add them while you are here. The send port’s parameters (Step 6) and the alert subject (Step 7) bind these values, and adapter parameters are plain strings: they bind {{…}} tokens but never evaluate a body path.

Promotions on Order
PromotionPath
OrderNumber$.orderNumber
CustomerId$.customer.id
OrderTotal$.total
Promotion on OrderInsertFailed

A failed order re-publishes payload intact, but promotions are type-qualified, so the exception type needs its own:

PromotionPath
OrderNumber$.orderNumber

2
Step Two
Create the Authentications

Three external parties, three credentials. Under the application’s Authentications, the dialog asks for a Name, the Adapter it pairs with, and a Definition, the credential shape that adapter offers, with the Application preset; the Definition decides the config section that follows.

Storefront credential, for the API Listener
SettingValue
NameStorefrontToken
AdapterAPI Listener
DefinitionAPI Listener Token
Token (Partner Config)the secret the storefront will present on every call
Order database, for the SQL Caller
SettingValue
NameOrderDbSql
AdapterSQL Caller
DefinitionSQL Server Connection
Connection String (Database Config)the order database’s connection string, credentials included
Alert mailbox, for O365 Mail
SettingValue
NameO365Ops
AdapterO365 Mail Sender
DefinitionMicrosoft Graph
Tenant Id / Client Id / Client Secret (Graph AuthConfig)an app registration with Mail.Send granted

3
Step Three
Create the database objects

Create the table and procedure in your database, the send port (Step 6) calls the procedure. 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
Build the inbound map
Create the map

Under the application’s Maps, create the map. A map is not a file, it is a named resource stored in the platform, defined by its name and the two message types it sits between, both created in Step 1.

Map settingValue
NameWebOrderToOrder
Source message typeWebOrder, from Step 1
Target message typeOrder, from Step 1
The sample WebOrder

A sample WebOrder as the storefront posts it, the same payload Step 8 tests with:

JSON
{
  "order_no": "10472",
  "ship_region": "EMEA",
  "buyer": { "ref": "C-2201", "company": "Nordwind Retail" },
  "account_mgr": "j.driessen@example.com",
  "grand_total": 1249.50
}
Author the XSLT

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 order number gets the SO- prefix every downstream tutorial expects, and 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>

5
Step Five
Create the receive port
General

Create a receive port WebshopOrders bound to the API Listener. Everything it references already exists, so each field is a pure selection. (The dropdowns can also create types and maps inline; building the dependencies first keeps each dialog a selection.) If you need 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.

GeneralValue
AdapterAPI Listener
WayOne
Pair the storefront credential

Open the adapter configuration (the button beside Adapter) and pair the port with the storefront credential, so every caller must present it:

Adapter ConfigurationValue
Auth Config TypeStatic
Auth ConfigStorefrontToken, from Step 2
VerbPOST
Headersthe two rows below
Declare the routing headers

Every API Listener in the environment shares one ingress URL, the read-only Url at the foot of the dialog, so the storefront never calls a port-specific address: the runtime decides which receive port a request belongs to from the App header, the Authentication, and any distinguishing headers (routing on a shared ingress covers the full rule). Declare both in the Headers table:

HeaderValueWhy
AppAcmeOrdersthe Application’s namespace, narrows the request to this Application
PurposeOrderIntakekeeps this Listener unique if the App later gains a second one (e.g. a stock lookup)
Set the fallback type

On the Publish step under the port’s Solicit stages, set Message Type Fallback to WebOrder, from Step 1.

Select the map

On the Map step, set Map Assignment to Typed and add one row, selecting the map from Step 4:

Source Message TypeMapTarget Message Type
WebOrderWebOrderToOrderOrder

6
Step Six
Create the SQL send port
General

Create a send port OrderToDatabase bound to the SQL Caller, paired with the OrderDbSql Authentication from Step 2. It subscribes on {{Message.MessageType}} == "Order".

SettingValue
AdapterSQL Caller
WayOne
Command TypeStored Procedure
Command Textdbo.UpsertOrder
Input Parameters

The procedure from Step 3 needs three values out of the payload; its parameters go under Input Parameters, one key/value row each, binding the promotions defined on Order in Step 1:

ParameterValue
OrderNumber{{Promoted.Order.OrderNumber}}
CustomerId{{Promoted.Order.CustomerId}}
OrderTotal{{Promoted.Order.OrderTotal}}

7
Step Seven
Wire the failure path
Set the Exception Message Type

On OrderToDatabase, set Exception Message Type, on the port’s General step, to OrderInsertFailed, from Step 1. If the insert exhausts its retries, the failed order re-publishes under that type, payload intact, and the promotion you defined on it carries the order number into the alert subject.

Create the OpsAlert send port

Add an O365 Mail send port OpsAlert subscribing on {{Message.MessageType}} == "OrderInsertFailed":

SettingValue
AuthenticationO365Ops, from Step 2
Fromnoreply@acme.example
Toops@acme.example
SubjectOrder insert failed: {{Promoted.OrderInsertFailed.OrderNumber}}

8
Step Eight
Start the ports and test
Start in dependency order

There is nothing to deploy, everything you configured is already saved and live, because Art2link applies changes immediately. To bring the flow online you start the send ports first, then the receive port: start OrderToDatabase and OpsAlert, then start WebshopOrders. Starting the subscribers 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.)

Turn on tracking

Before you test, set Tracking to Enabled + Body on every port the flow touches, WebshopOrders, OrderToDatabase, and OpsAlert. That records each run step by step with the message body, so you can walk a single order end to end in tracking and see the payload exactly as it was mapped and inserted.

Post a test order

POST the Step 4 sample WebOrder to the shared ingress URL, the one base URL every API Listener in the environment is hosted under (see routing on a shared ingress), and present the three things the Listener routes on: the App: AcmeOrders header, the Purpose: OrderIntake header, and the storefront’s API Authentication credential. Verify in tracking that the run shows the map and the insert; check the row landed with order number SO-10472.

Break it on purpose

Stop the database or post an order that violates a constraint, and confirm the operations mailbox gets the OrderInsertFailed alert and the run is available for replay.

Turn bodies off in production. Enabled + Body is the most expensive tracking level, it costs extra processing and database space and stores every payload that flows, including successful runs. Once the flow is proven, set the ports to Only on Error instead: failures still capture the full body for diagnosis, but healthy runs are not recorded, so you keep the evidence where it matters without paying for the rest.