Skip to content

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 from TariffLines (placeholder codes like "DUMMY COMMODITY C" are dropped for the declarant to fill); SalesInvoiceLines are commercial detail and are not mapped. The embedded base64 <PDF> is stored as the dossier's invoice document via the canonical attachments mechanism.

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:

  • yyyyMMdd dates 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/regimeFormer map to requested_procedure/previous_procedure per direction.
  • docCat AU documents become authorisations; all other documents keep their category (SD/TD/RD).
  • declCreateTransit is 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:

  1. ReceiveCustomsFileForDescartes — queue trigger, reads the message
  2. ContextProps — extracts message properties (filename, message type, etc.)
  3. Try Process XML — decompresses / validates the payload (if applicable)
  4. HTTP LogRcv — logs receipt to an internal endpoint
  5. ArchiveFile — saves a copy to archive storage
  6. 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_dispatch AppSetting): when true, 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 Partner application property set — Qargo, VDM, or any future integration simply publishes to the same queue.
  • Connectivity probe: /Admin/IngestionSettingsRun peek probe checks reachability of the inbound queue without consuming messages.