Outbound Messages
Outbound messages are messages that originate from Business Central to an external system. The messages get triggered by pre-defined business events or background processes.
Outbound Integration Handlers
Outbound integration handlers are created for every outbound message you wish to send to an external system. The integration handler is used to define:
- The outbound event used to trigger the message
- Message Type, EDI Partner, etc.
- The main table that the message is based on
- The payload generation method used to build the outbound payload.
- The API Endpoint
- Additional subscribers (e.g. when you send the same message to multiple partners)
- If you want a delay in the messages. For example, suppose you are posting a large warehouse shipment. In that case, you should delay sending a posted invoice until you are sure everything on the warehouse shipment has been posted to prevent you from sending a message while posting a transaction and later having that transaction rollback.
- Remove Special Characters – Specifies that special characters are removed from the message during HTTP calls when the message is encoded as Base64 text. This is a per-handler override; if Remove Special Characters is already enabled on the API Endpoint, the characters are removed regardless of this setting.
- Delete Data Exchange Payload – When enabled, temporary Data Exchange records used to build payloads are deleted after processing. Keep this enabled to reduce storage growth unless you need to inspect Data Exchange records for troubleshooting.
Payload Generation Method
Outbound handlers support four payload generation methods:
| Method | Configuration | Purpose |
|---|---|---|
| Data Exchange Definition | Set Data Exch. Def. Code | Uses a Data Exchange Definition mapping to export the payload from the selected record(s). This is the default option. |
| Custom | — | Allows a custom payload to be provided or generated outside Data Exchange Definition mappings. |
| XMLport | Set Export XMLport No. | Runs a standard Business Central XMLport to produce the outbound payload. |
| Codeunit | Set Export Codeunit No. | Runs a custom codeunit that generates the payload and returns it to Integration Hub. |
Additional Subscribers continue to reuse the payload generated by the parent handler.
Custom Payload
The payload can be supplied in one of two ways:
- Prebuilt payload passed from another extension (for example, an EFT file generated by the Australia/New Zealand Accelerator and handed to Integration Hub for delivery to the bank).
- Subscriber-generated payload via extension event handling by subscribing to
OnGenerateCustomPayloadin codeunit OutboundEventMgt_IH_TSL.
XMLport Export
Integration Hub calls Xmlport.Export on the specified object, passing the source record (with all applied filters) as the data source. The XMLport receives a typed record variable with the same filters that the outbound handler applied — whether that is a single record (Record-Based granularity) or a set of records matching a filter (Filter-Based granularity).
Your XMLport's data source table should match the Table ID configured on the outbound handler. The filters are already applied, so the XMLport simply iterates the records it receives.
Codeunit Export
Integration Hub calls Codeunit.Run on the specified object, passing the source record (with all applied filters) as the Variant parameter. The codeunit receives the filtered record via its OnRun trigger and must build the payload and return it by calling SetExportPayloadBlob on the SessionValues_IH_TSL singleton codeunit.
The record has the same filters applied by the outbound handler — for Record-Based granularity this is a single record, and for Filter-Based granularity this is the full set of matching records. The codeunit has full access to the record and can read related tables, call external services, or generate any format required.
To implement a Codeunit export:
- Create a new codeunit.
- Set
TableNoto the table that your outbound handler is based on (e.g.TableNo = "Sales Header"). This allows Business Central to pass the filtered record viaCodeunit.Run. - In the
OnRuntrigger, useRecto access the source record. All filters from the outbound handler are already applied — you can iterate withFindSetor read a single record depending on your scenario. - Generate your payload content and write it to a
Temp Blob. - Pass the
Temp Blobback to Integration Hub by callingSessionValues_IH_TSL.SetExportPayloadBlob. - Set the Export Codeunit No. on your outbound handler to this codeunit's ID.
codeunit 50100 "My Export Codeunit"
{
TableNo = "Sales Header";
trigger OnRun()
var
SessionValues: Codeunit SessionValues_IH_TSL;
TempBlob: Codeunit "Temp Blob";
OutStr: OutStream;
Content: Text;
begin
// Rec already has the filters applied by the outbound handler.
// For Record-Based granularity, Rec is filtered to a single record.
// For Filter-Based granularity, Rec contains all matching records.
Content := BuildPayload(Rec);
TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8);
OutStr.WriteText(Content);
// Return the payload to Integration Hub.
SessionValues.SetExportPayloadBlob(TempBlob);
end;
local procedure BuildPayload(var SalesHeader: Record "Sales Header"): Text
begin
// Your custom payload generation logic here.
// Use SalesHeader.FindSet() to iterate if multiple records are expected.
exit('{"OrderNo":"' + SalesHeader."No." + '"}');
end;
}
If SetExportPayloadBlob is not called, or if the Temp Blob has no value, Integration Hub will raise an error indicating that the codeunit did not produce a payload. Ensure all code paths that should return a payload call SetExportPayloadBlob.
The SessionValues_IH_TSL codeunit is a SingleInstance codeunit used as a session-scoped state container within Integration Hub. Calling SetExportPayloadBlob stores the payload in session memory, where Integration Hub retrieves it immediately after Codeunit.Run returns.
Outbound Granularity
The Outbound Granularity field on the Outbound Integration Handler controls how records are grouped into outbound messages.
| Option | Behaviour | When to use |
|---|---|---|
| Record-Based (default) | Creates one message per record. Each record (along with any child lines defined in the Data Exchange Definition) is exported as a separate payload. | EDI scenarios, event-based exports, or any case where each document should be sent individually — for example, one posted invoice or one posted shipment per message. |
| Filter-Based | Creates a single message containing all records that match the applied filter. All matching records are consolidated into one payload. | CSV exports, batch file transfers, or scenarios where the receiver expects multiple records in a single file — for example, a daily export of all modified customers or a consolidated order file. |
Record-Based is the traditional Integration Hub behaviour and is the correct setting for existing exports. Only change to Filter-Based when you specifically need to consolidate multiple records into one file.
Outbound Events
You can view a list of available outbound events from the outbound events page. You can also add additional events with an extension.
The following events are more generic:
Synchronise in the background using ModifiedAt date/time (per record)
This outbound event gets triggered by running the Outbound Synchronisation Runner code unit. You can use this event for all tables in Business Central.
The following additional fields are available on the integration handler when using this event:
| Field | Purpose |
|---|---|
| Synch. Wait Time (m/s) | Specifies a delay in milliseconds between each individual record-based message. This throttles the rate of dispatch to help avoid hitting external API rate limits. Only applies when Outbound Granularity is Record-Based. |
| Run in Background | When enabled, a separate Job Queue Entry is created to process the queued instruction in the background. When disabled, the instruction is processed immediately within the Sync Runner's session. See How Background Sync Processing Works for details. |
| Background Job Category | Specifies the Job Queue Category assigned to the background Job Queue Entry. All entries in the same category are processed sequentially, which can be used to control the order of execution. |
| Background Run Date Formula | Specifies a date formula that delays the start of the background Job Queue Entry. For example, a formula of 1D means the instruction will not be processed until at least one day after it was created. |
Generic record triggered by custom extension (with payload support)
This event is intended to be used by an extension to trigger an outbound message for a specific table. Your extension can fire this event and pass any record to process the outbound message.
Use codeunit OutboundEventHandling_IH_TSL with one of these procedures:
- RunOutboundEventHandlerForAGenericRecordTriggeredByACustomExtension - raises the generic outbound event using normal payload generation.
- GenericRecordTriggeredByCustomExtensionWithPayload - raises the same outbound event with a prebuilt payload.
Warehouse Shipment is created (After warehouse shipment posting)
This outbound event fires after a warehouse shipment is posted (OnAfterPostWhseShipment). Use this event when you need to notify an external system of shipped goods, such as sending an advance shipping notice (ASN).
Additional Subscribers on Outbound Messages
Occasionally, you must send the same message to multiple locations. You can specify this in the Additional Subscribers section on the outbound integration message handler card. When a message is sent as an additional subscriber, it uses the original message that is exported. This approach simplifies the configuration and reduces the amount of reads to handle this.
Snapshot Data Check Mode
Snapshot Data Check Mode controls whether outbound messages are sent for every record modification or only when specific tracked fields have changed.
Standard Mode (default)
Every update to a record triggers a message, regardless of which fields changed.
Snapshot Mode (Configured Fields Only)
When enabled, the system tracks the last-exported values of configured fields. A message is only sent when one or more of those fields has changed since the last export. After sending, the stored values are updated.
This is useful for high-volume tables where only certain field changes are relevant to the receiving system — for example, sending a customer update only when the address or payment terms change, not when an internal note is modified.
Sync Watermark Behaviour with Snapshot
When using the Synchronise in background using ModifiedAt event, the sync watermark (Synch. Modified On Filter) advances at the end of every sync run — even if the snapshot determined that no tracked fields had changed and no messages were sent.
This is intentional. Without this behaviour, records that have been modified but contain no relevant field changes would be re-evaluated on every subsequent sync cycle, causing unnecessary database reads. By advancing the watermark regardless, those records are skipped in future runs and only re-evaluated if they are modified again.
How to Enable Snapshot Mode
- Open the Outbound Integration Handler card.
- Enable Snapshot Data Check Mode.
- Choose Configure Snapshot Fields and select the fields that should trigger outbound messages when changed.
Snapshot values are stored as text. If a field value exceeds 250 characters, it cannot be tracked for changes and the system will always treat it as changed.
Updating Snapshot Data in Custom Export Implementations
When Payload Generation Method is set to Data Exchange Definition, Integration Hub automatically updates snapshot field values for each exported record. This ensures that subsequent sync runs only re-export records whose tracked fields have actually changed.
However, when using XMLport, Codeunit, or Custom payload generation with Filter-Based granularity, snapshot data is not updated automatically. As a result, if Snapshot Data Check Mode is enabled, the same records will be re-exported on every sync run because the system still considers their tracked fields as "changed".
To resolve this, your custom export implementation must explicitly update snapshot data for each record it processes. Use the UpdateSnapshotForExportedRecord procedure on the SnapshotManager_IH_TSL codeunit.
If you use Filter-Based granularity with Snapshot Data Check Mode enabled and a Payload Generation Method other than Data Exchange Definition, you must call UpdateSnapshotForExportedRecord for each exported record. Failing to do so will cause duplicate messages on every sync cycle.
Codeunit Example with Snapshot Update
codeunit 50101 "My EDIFACT Export"
{
TableNo = "Sales Invoice Header";
trigger OnRun()
var
SalesInvLine: Record "Sales Invoice Line";
SessionValues: Codeunit SessionValues_IH_TSL;
SnapshotManager: Codeunit SnapshotManager_IH_TSL;
TempBlob: Codeunit "Temp Blob";
OutStr: OutStream;
IntHandlerCode: Code[20];
Payload: TextBuilder;
LineCount: Integer;
begin
IntHandlerCode := SessionValues.GetCurrentIntegrationHandlerCode();
// Build an EDIFACT INVOIC message for each invoice in the filtered set.
if Rec.FindSet() then
repeat
SnapshotManager.UpdateSnapshotForExportedRecord(Rec, IntHandlerCode);
Payload.AppendLine('UNH+' + Rec."No." + '+INVOIC:D:96A:UN''');
Payload.AppendLine('BGM+380+' + Rec."No." + '+9''');
Payload.AppendLine('DTM+137:' + Format(Rec."Posting Date", 0, '<Year4><Month,2><Day,2>') + ':102''');
Payload.AppendLine('NAD+BY+++' + Rec."Bill-to Name" + '''');
SalesInvLine.SetRange("Document No.", Rec."No.");
if SalesInvLine.FindSet() then begin
LineCount := 0;
repeat
LineCount += 1;
Payload.AppendLine('LIN+' + Format(LineCount) + '++' + SalesInvLine."No." + '''');
Payload.AppendLine('QTY+47:' + Format(SalesInvLine.Quantity) + '''');
Payload.AppendLine('MOA+203:' + Format(SalesInvLine."Line Amount") + '''');
until SalesInvLine.Next() = 0;
end;
Payload.AppendLine('UNT+' + Format(LineCount + 5) + '+' + Rec."No." + '''');
until Rec.Next() = 0;
TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8);
OutStr.WriteText(Payload.ToText());
SessionValues.SetExportPayloadBlob(TempBlob);
end;
}
XMLport Example with Snapshot Update
When using an XMLport for payload generation, you can call UpdateSnapshotForExportedRecord directly from within the XMLport's export triggers. No wrapper codeunit is required:
xmlport 50100 "My Sales Export"
{
Caption = 'My Sales Export';
Direction = Export;
Format = Xml;
UseRequestPage = false;
schema
{
textelement(Root)
{
tableelement(SalesHeader; "Sales Header")
{
fieldattribute(No; SalesHeader."No.") { }
fieldelement(CustomerName; SalesHeader."Sell-to Customer Name") { }
fieldelement(PostingDate; SalesHeader."Posting Date") { }
trigger OnAfterGetRecord()
begin
SnapshotManager.UpdateSnapshotForExportedRecord(SalesHeader, SessionValues.GetCurrentIntegrationHandlerCode());
end;
}
}
}
var
SessionValues: Codeunit SessionValues_IH_TSL;
SnapshotManager: Codeunit SnapshotManager_IH_TSL;
}
Custom Payload Example with Snapshot Update
There are two approaches depending on how the payload is generated:
Option 1: Prebuilt payload (e.g. EFT file from another extension)
The payload is already generated before it is passed to Integration Hub via GenericRecordTriggeredByCustomExtensionWithPayload. In this case, update the snapshot after passing the payload:
Generate payload (e.g. EFT file)
Call GenericRecordTriggeredByCustomExtensionWithPayload(Record, PayloadBlob)
For each record in the set:
SnapshotManager.UpdateSnapshotForExportedRecord(Record, HandlerCode)
Option 2: Subscriber-generated payload via OnGenerateCustomPayload
Your subscriber builds the payload inline. Update the snapshot as you iterate each record:
Subscribe to OnGenerateCustomPayload
Filter to your handler code
For each record in the set:
SnapshotManager.UpdateSnapshotForExportedRecord(Record, HandlerCode)
Append record data to payload
Write payload to PayloadTempBlob
Set IsHandled := true
When using Record-Based granularity, each message contains a single record. The snapshot update is still recommended but has lower risk — without it, the record would simply be re-exported once on the next sync run. With Filter-Based granularity, the entire batch of records is affected, making the snapshot update essential.
Direct Calls to Endpoints
By default, Integration Hub wraps outbound messages in a JSON payload when communicating via Azure API Management (APIM). If you are integrating directly with a REST API — without APIM in between — you can use Direct Call mode to send the Data Exchange output as the raw HTTP body instead.
The following fields are available on the Outbound Integration Handler:
| Field | Purpose |
|---|---|
| Use Direct Call | When enabled, the integration handler sends the Data Exchange output directly to the API endpoint as the HTTP body, bypassing the standard JSON envelope. Do not use this with APIM. |
| Direct Call Content Type | Specifies the HTTP Content-Type header for the direct call. Defaults to application/json. |
Handling Responses from Outbound HTTP Calls
When making direct HTTP calls to external endpoints, Integration Hub can process the response body returned by the endpoint as an inbound message. Enable the Handle Return Message field on the Outbound Integration Handler, and configure a corresponding Inbound Integration Handler to handle the response.
This is useful when an external API returns a result that needs to be processed — for example, receiving a confirmation ID or a status payload immediately after posting.
Handle Return Message is not compatible with the Skip Error on Failed Call option on the API Endpoint.
Update Values after Export
If you want to update a record immediately after it has been sent, you can specify values to be updated on the integration handler on the After Export Tab. You can optionally choose to run the modify trigger.
When you run the trigger, there is a possibility for recursion if you do not have filters that exclude this. The program will error if it detects recursion.
Import and Export Integration Handlers
The Outbound Integration Handlers page includes two actions:
- Export Integration Handlers
- Import Integration Handlers
Use these actions to move outbound integration handler configurations between environments (for example, from Sandbox to Production).
The export/import package includes the selected handler records and their related setup, including referenced tables and Data Exchange Definitions used by those handlers.
Data Exchange Definitions
The table below specifies the settings to use on Data Exchange definitions for exporting data.
| File Type | Type | Data Handling Codeunit | Reading/Writing Codeunit | Reading/Writing XMLport | User Feedback Codeunit |
|---|---|---|---|---|---|
| XML | Integration Hub Export | 70254543 | 70254543 | 70254544 | |
| Json | Integration Hub Export | 70254543 | 70254543 | 70254544 | |
| Variable Text (CSV) | Integration Hub Export | 70254543 | 70254543 | 70254542 | 70254544 |
Select Integration Hub Export as the Type and the codeunit fields above will be populated automatically based on the File Type you choose. You no longer need to enter these values manually. Selecting this type does not affect any downstream processing — it is only used to apply these defaults.
CSV Export (Variable Text)
Integration Hub supports exporting data as CSV (Variable Text) files using the Data Exchange framework. When you select Integration Hub Export as the Type and Variable Text as the File Type, the system automatically configures the Reading/Writing Codeunit and a dedicated CSV XMLport that honours your encoding, field separator, and line separator settings.
Setting up a CSV Export
- Create a new Data Exchange Definition.
- Set Type to Integration Hub Export.
- Set File Type to Variable Text.
- The Reading/Writing Codeunit and Reading/Writing XMLport are populated automatically.
- Configure your Line Definitions and Column Definitions as usual — the column Name property is used for headings (see below).
- Add a Data Exchange Mapping for the table you want to export from. The Mapping Codeunit is defaulted automatically when you save the mapping.
Include Column Headings
When exporting to Variable Text or Fixed Text, you can enable Include Column Headings on the Data Exchange Definition. When enabled, the first row of the exported file contains column names taken from each column definition's Name field.
This is useful when the receiving system expects a header row, or when the file will be opened in Excel for review.
Export Line Numbering
Prior to version 27.0.10.0, multi-level Data Exchange exports assigned line numbers using a gap-based scheme — each level received a large increment (e.g. 10000000 for level 1, 10000 for level 2, 100 for level 3) to leave room for child lines. This approach imposed hard limits on the total number of records that could be exported per level (since the available number range would be exhausted) and added unnecessary complexity.
From version 27.0.10.0, export line numbers are assigned sequentially (1, 2, 3…) regardless of level. This removes the record-count limitation and simplifies the internal processing logic.
These line numbers exist only in the intermediate Data Exch. Field table during file generation — they are not written to the output file or payload. Standard exports and standard Data Exchange mappings are unaffected.
You are only affected if you have a custom subscriber that reads line numbers from Data Exch. Field records during export processing and relies on the gap-based numbering to determine hierarchy or parent–child relationships. If so, update your subscriber to use sequential numbering or the column/line definition structure instead.
Outbound Job Queue Processing
Integration Hub uses two Job Queue codeunits to handle outbound messages in the background:
How Background Sync Processing Works
The Outbound Sync Runner (OutIntSyncRunner_IH_TSL) is scheduled as a recurring Job Queue Entry. When it runs, the following happens for each matching handler:
- Identify records to sync — The runner queries records where
SystemModifiedAtfalls within the sync window (from last sync timestamp to now). - Create an Integration Job Queue Entry — An instruction record is created in the Queued Integration Messages table, capturing the handler code and the sync date/time window.
- Advance the handler timestamp — The
Synch. Modified On Filteris updated immediately so that subsequent runs do not re-process the same records. - Dispatch the instruction — How this instruction is processed depends on the Run in Background setting:
| Run in Background | Behaviour |
|---|---|
| Disabled (default) | The instruction is processed immediately inline within the Sync Runner's session. This means a single Job Queue Entry (the Sync Runner) handles both identifying records and sending messages. |
| Enabled | A separate Job Queue Entry is created to process the instruction in the background. This means two Job Queue stages are involved: the Sync Runner identifies and queues, and a second job processes and sends. |
Regardless of the Run in Background setting, if an error occurs during processing, it is recorded on the Integration Job Queue Entry in the Queued Integration Messages page. The entry will show a status of Error with the error message. This is where you should look to diagnose sync failures — not in the Job Queue Entries page.
Enable Run in Background when you want the Sync Runner to quickly queue instructions for multiple handlers without waiting for each to finish sending. This is useful when you have many handlers or large record sets and want to parallelise the actual message dispatch. When disabled, the sync runner processes each handler sequentially and only moves to the next handler after the current one finishes.
Outbound Sync Runner (OutIntSyncRunner_IH_TSL)
Identifies records that have been modified since the last sync and creates Queued Integration Messages for processing. This is the entry point for all handlers using the Synchronise in background using ModifiedAt event.
- Parameter String — Leave empty to process all enabled sync handlers, or provide a filter on the integration handler code to target specific handlers.
Outbound Queue Processor (ProcessOutIntJQEntry_IH_TSL)
Processes Queued Integration Messages — picking up ready entries, generating the payload, and dispatching them to the configured API endpoint. This handles both delayed messages (created when a delay is specified on the handler) and messages queued by the Sync Runner.
- Parameter String — Leave empty to process all queued entries, or provide a filter on the integration handler code.
The app prompts you to configure these Job Queue Entries when you enable a delay on a handler or select an outbound event that requires background processing. It also checks when you open the Outbound Integration Handlers page or the Queued Integration Messages page.
Testing Outbound Integration Handlers
Choose "Export Message". The following applies:
- A filter page opens where you can specify filters for the record.
- You will receive a confirmation message if multiple records exist within the filters specified, and you can choose to cancel or continue
- Each record (within the filters specified) is then published using the integration handler.
When Payload Generation Method is Custom, an additional prompt appears after you choose the record filter:
- Provide payload now
- Run without payload (raise subscriber event)
If you choose Provide payload now:
- You are prompted to upload a payload file.
- The selected filter must resolve to a single record.
If you choose Run without payload (raise subscriber event):
- The system runs the standard publish flow for each record in the selected filter.
- A custom subscriber event is expected to provide the payload. Subscribe to OnGenerateCustomPayload in codeunit OutboundEventMgt_IH_TSL.
If Payload Generation Method is Custom and no prebuilt payload is provided, the publish requires a subscriber to generate payload content. If no payload is generated, processing stops with an error.
This process does not run the outbound event - it behaves as if the outbound event had been triggered.
Troubleshooting
XML Root Element Error
Error: Cannot navigate XML path because the root element does not exist in the XML Buffer.
This error occurs when the Integration Hub attempts to build an XML payload using a Data Exchange Definition but cannot find the root element in the XML Buffer.
Cause: The most common cause is an incorrectly defined Data Line Tag on the Data Exchange Line Definition. The Data Line Tag specifies the XML path structure for the export and must begin with the correct root element.
Resolution:
- Open the Data Exchange Definition referenced by your outbound integration handler.
- Navigate to the Line Definitions section.
- Check the Data Line Tag value — it must start with a valid root element path (e.g.
/Root/Child). - Ensure the path matches the structure you expect in your output XML.
- Verify that all Column Definitions also have correct Path values that are consistent with the Data Line Tag.
If you have multiple Line Definitions, ensure that parent/child relationships are correctly defined and that each line's Data Line Tag is a valid path within the overall document structure.
Example Fix
Your Data Exchange Definition needs a wrapper root element. Change the Data Line Tags to include the root:
| Line Def | Current Data Line Tag | Should be |
|---|---|---|
| INVOICEHEADER | /Invoice | /Invoices/Invoice |
| INVOICELINE | /Invoice/Lines/Line | /Invoices/Invoice/Lines/Line |