Azure Service Bus — Integration Architecture
Overview
CustomsHive connects to external systems via two Azure Service Bus queues:
| Queue | Direction | Purpose |
|---|---|---|
| Canonical ingestion queue | → CustomsHive | External systems push CanonicalDeclaration messages (JSON or XML) |
| Outbound Descartes queue | CustomsHive → | Generated SMF XML ready for SFTP upload to customs |
Both queues authenticate via ClientSecretCredential using the shared Azure:* credentials block.
Inbound — canonical ingestion queue
Configured under ServiceBus:Ingestion:*. CanonicalServiceBusReceiverService (in CustomsHive.Module.Ingestion) listens on the queue, deserializes messages as CanonicalDeclaration, and calls ICanonicalIngestionService.IngestAsync().
Configuration
"ServiceBus": {
"Ingestion": {
"ActiveEnvironment": "prd",
"prd": {
"FullyQualifiedNamespace": "your-servicebus.servicebus.windows.net",
"QueueName": "customshive-inbound"
}
}
}
Multiple named environments can be defined (dv, stg, prd). Only ActiveEnvironment is connected at runtime.
Message format
The body is either a canonical CanonicalDeclaration — see schemas/canonical/declaration.schema.json (JSON) or schemas/canonical/declaration.xsd (XML) — or one of the sender-native XML formats the receiver detects automatically:
- Ziegler/Z-CUSTOMS PRE-DECL (root
<Header>with a<Declaration>child) — see below. - Delta Light pro-forma invoice (root
<MT_ProFormaInvoice>, SAP PI/PO) — one EX declaration per message. Goods items come fromTariffLines(placeholder codes like "DUMMY COMMODITY C" are dropped for the declarant to fill);SalesInvoiceLinesare commercial detail and are not mapped. The embedded base64<PDF>is stored as the dossier's invoice document via the canonicalattachmentsmechanism.
JSON body example (T2 groupage):
{
"declaration_type": "T2",
"parties": {
"consignee": {
"name": "ACME SA",
"eori": "BE0123456789",
"city": "Brussels",
"country": "BE"
}
},
"transport": {
"departure_means_id": "1-ABC-123"
},
"totals": { "gross_mass": 2450.5 },
"house_consignments": [
{
"sequence_number": 23,
"gross_mass": 1200.0,
"reference_number_ucr": "UCR-2026-001",
"consignee": { "name": "Buyer NL BV", "country": "NL" }
}
],
"goods_items": [
{
"sequence_number": 1,
"description": "Machine parts",
"gross_mass": 1200.0,
"net_mass": 1100.0,
"house_consignment_sequence": 23,
"packages": [{ "type": "CT", "quantity": 5, "shipping_marks": "MARK-001" }]
}
]
}
XML body example: use Content-Type: application/xml or the ContentType message property.
Ziegler PRE-DECL bodies
A PRE-DECL is a combined message: declCreateExport / declCreateImport flags with per-direction Export/Import sub-blocks. The flags describe everything the customer wants created somewhere — historically the sending system created its own side and passed the counterpart's data to a foreign agent, so the import side of e.g. a BE→GB shipment is usually not ours to declare. Send the DeclarationType property (EX or IM) to create only the side we handle; without it, ZieglerPreDeclConverter fans the message out into every flagged type (one dossier per type). Conversion notes:
yyyyMMdddates become ISO 8601; decimal package counts (22.000) are rounded to whole numbers.- Export items get the 8-digit CN code (
taric1); import items the full 10-digit TARIC code (taric1+taric2). - Per-item
regimeAsked/regimeFormermap torequested_procedure/previous_procedureper direction. docCat AUdocuments become authorisations; all other documents keep their category (SD/TD/RD).declCreateTransitis ignored (no sender uses it; the format carries no guarantees/route) — the declarant creates the T-dossier manually.- Fields the format cannot express (coded goods-location on the import side, exchange rate, …) stay empty and are completed by the declarant in the review UI — every ingested dossier has
RequiresReview = true. - Incoming PRE-DECL bodies are validated against the official "Z-Customs Interface XML" schema (
schemas/z-customs/, embedded in CustomsHive.Core). Deviations are warnings, not rejections: they are logged (and shown in the UI for manual uploads) because they usually mean a sender-side bug whose data would otherwise be dropped silently, but a convertible message still creates its dossiers.
Message properties
| Application property | Type | Required | Description |
|---|---|---|---|
Partner |
string | yes | Identifies the sending system (e.g. "Qargo", "VDM"). Stored as Dossier.IngestionPartner. |
Ucr (alias DossierNumber) |
string | no | Dossier reference from the sender (e.g. Navision dossier number). Used as the dossier UCR; when one message yields multiple dossiers it is suffixed with the declaration type ({ucr}-EX, {ucr}-IM). Never overrides a UCR set in the body. Absent → auto-generated. |
ClientCode |
string | no | Navision client code. Authoritative: overrides any client code in the body. Unknown codes auto-create an unverified client (admin verifies before use in declarations); without it, client resolution falls back to the relevant party's EORI. |
DeclarationType |
string | no | For combined PRE-DECL bodies: which side to create (EX or IM). Requesting a type the message does not flag fails the message. Absent → every flagged type is created. |
ContentType message property: application/json (default) or application/xml. When absent, the receiver auto-detects by checking if the body starts with <.
Flow
flowchart TD
SB["ServiceBus\ncanonical queue"]
RCV["CanonicalServiceBusReceiverService\npeak-lock / complete"]
IS["ICanonicalIngestionService"]
DB[(Dossier\nSource=ServiceBusIngestion)]
SB -->|JSON or XML body| RCV
RCV -->|CanonicalDeclaration + Partner| IS
IS --> DB
Outbound — Descartes queue
Configured under ServiceBus:Descartes:*. DescartesServiceBusSenderService (in CustomsHive.Module.Descartes) publishes SMF-wrapped XML messages after a declaration is generated.
Configuration
"ServiceBus": {
"Descartes": {
"ActiveEnvironment": "prd",
"prd": {
"FullyQualifiedNamespace": "your-servicebus.servicebus.windows.net",
"QueueName": "customshive-outbound"
}
}
}
Message format
{
"dossier_id": 42,
"ucr": "TUF/2026/001",
"message_type": "CC015C",
"filename": "SMF_NCTS_CC015C_TUF2026001_20260518.xml",
"xml_base64": "<base64-encoded SMF XML>"
}
Downstream: Azure Logic App (prc-descartes-xml)
A Logic App in the Azure tenant triggers on this queue and handles delivery. Steps:
- ReceiveCustomsFileForDescartes — queue trigger, reads the message
- ContextProps — extracts message properties (filename, message type, etc.)
- Try Process XML — decompresses / validates the payload (if applicable)
- HTTP LogRcv — logs receipt to an internal endpoint
- ArchiveFile — saves a copy to archive storage
- UploadFile — uploads the XML file via SFTP to the customs authority
The file is delivered as { "$content-type": "application/xml", "$content": "<base64>" } using the filename from the message payload.
Full flow diagram
flowchart TD
subgraph External["External systems (e.g. Qargo, VDM)"]
Q["Logistics platform\n(per trip / shipment)"]
end
subgraph ASB_IN["Azure Service Bus — inbound"]
MSG_IN["CanonicalDeclaration\n(JSON or XML)"]
end
subgraph CSHIVE["CustomsHive"]
RCV["CanonicalServiceBusReceiverService"]
IS["ICanonicalIngestionService\nclient resolution + adapter routing"]
DB[(Database\ndossiers, correctedData)]
GEN["XML Generators\nCC015C / IE415B / CC515C"]
end
subgraph ASB_OUT["Azure Service Bus — outbound"]
MSG_OUT["SMF XML\n(base64-encoded)"]
end
subgraph DESC["Azure Logic App — prc-descartes-xml"]
LA_RCV["ReceiveCustomsFileForDescartes\n(queue trigger)"]
LA_CTX["ContextProps"]
LA_XML["Try Process XML\n(decompress / validate)"]
LA_LOG["HTTP LogRcv"]
LA_ARC["ArchiveFile"]
LA_UPL["UploadFile\n(SFTP)"]
end
Q -->|"publish CanonicalDeclaration\n+ Partner property"| MSG_IN
MSG_IN -->|peek-lock / complete| RCV
RCV --> IS
IS -->|create dossier\nSource=ServiceBusIngestion| DB
DB -->|user triggers or auto-dispatch| GEN
GEN -->|SMF XML| MSG_OUT
MSG_OUT --> LA_RCV
LA_RCV --> LA_CTX --> LA_XML --> LA_LOG --> LA_ARC --> LA_UPL
Notes
- Peek-lock is used on the inbound queue so a crash before
Complete()automatically re-queues the message. - Auto-dispatch (
ingestion:auto_dispatchAppSetting): whentrue, automatically sends the generated T2 XML to the Descartes outbound queue after a dossier is created from a canonical ingestion message. Managed via/Admin/IngestionSettings. - The canonical queue accepts any sender that has the
Partnerapplication property set — Qargo, VDM, or any future integration simply publishes to the same queue. - Connectivity probe:
/Admin/IngestionSettings→ Run peek probe checks reachability of the inbound queue without consuming messages.