Skip to content

Ingestion API

External systems can push customs declarations directly into CustomsHive using the canonical ingestion contract. Two entry points share the same ingestion pipeline:

Entry point Auth Format
POST /api/declarations X-Api-Key header or Azure AD Bearer (Ingestion.Write role) JSON or XML body
Azure Service Bus canonical queue Service Bus RBAC / SAS JSON or XML message body

Both paths call ICanonicalIngestionService, which resolves the client, routes to the correct adapter, and creates a dossier. See servicebus-integration.md for the ServiceBus-specific setup.


REST API — POST /api/declarations

Authentication

Two methods are supported; either is sufficient:

1. API Key (recommended for machine-to-machine)

Add an X-Api-Key header with a key generated in /Admin/ApiKeys. Keys are hashed (SHA-256) in the database — the raw key is shown only once at creation.

POST /api/declarations
X-Api-Key: ck_live_abc123...
Content-Type: application/json

2. Azure AD Bearer token (Ingestion.Write role)

Assign the Ingestion.Write app role (defined on the CustomsHive app registration in Entra ID) to the calling app registration or user, then send a Bearer token:

POST /api/declarations
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciO...
Content-Type: application/json

This path also works for interactive testing — sign in via Swagger UI at /swagger (dev/dv environments only) and use the Bearer security scheme.

Request body

Content-Type: application/json or Content-Type: application/xml.

The body must conform to the canonical declaration schema: - JSON: schemas/canonical/declaration.schema.json - XML: schemas/canonical/declaration.xsd

Property names are case-insensitive. The recommended format is snake_case (matching the JSON Schema).

Only declaration_type is required. All other fields are optional — the ingestion service fills in AppSettings-sourced company defaults before routing to the adapter.

Response

201 Created on success:

{
  "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "ucr": "TUF-20260613123456-abc123",
  "regime": "T2",
  "status": "Draft"
}

4xx on failure:

Status Reason
400 Body empty or could not be parsed
401 No valid X-Api-Key and no authenticated user with Ingestion.Write role
422 declaration_type is missing
429 Rate limit exceeded (60 req/min)

Canonical declaration format

Full schema: schemas/canonical/declaration.schema.json

Minimal examples by regime

IM — import:

{
  "declaration_type": "IM",
  "client_code": "SKCH",
  "parties": {
    "exporter": { "name": "Supplier Co", "country": "TR", "eori": "TR1234567890" }
  },
  "shipment": {
    "invoice_number": "INV-2026-001",
    "invoice_date": "2026-06-01",
    "currency": "EUR",
    "total_invoice_amount": 15000.00,
    "incoterm": "DAP"
  },
  "totals": { "gross_mass": 850.0 },
  "goods_items": [
    {
      "sequence_number": 1,
      "description": "Textile goods",
      "commodity_code": "6203420090",
      "country_of_origin": "TR",
      "gross_mass": 850.0,
      "net_mass": 800.0,
      "packages": [{ "type": "CT", "quantity": 10 }]
    }
  ]
}

EX — export:

{
  "declaration_type": "EX",
  "parties": {
    "exporter": { "eori": "BE0123456789", "name": "Exporter NV" },
    "consignee": { "name": "US Importer LLC", "country": "US" }
  },
  "customs_offices": {
    "export": "BE000100",
    "exit": "BE000200"
  },
  "transport": { "mode_at_border": 1, "border_means_id": "1-XYZ-789" },
  "goods_items": [
    {
      "sequence_number": 1,
      "description": "Machine parts",
      "commodity_code": "8479899700",
      "gross_mass": 500.0,
      "net_mass": 480.0,
      "packages": [{ "type": "BX", "quantity": 2 }]
    }
  ]
}

T2 — transit groupage (Qargo pattern):

{
  "declaration_type": "T2",
  "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", "city": "Amsterdam" }
    },
    {
      "sequence_number": 24,
      "gross_mass": 1250.5,
      "reference_number_ucr": "UCR-2026-002",
      "consignee": { "name": "Buyer CH AG", "country": "CH", "city": "Zurich" }
    }
  ],
  "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" }]
    },
    {
      "sequence_number": 2,
      "description": "Electronics",
      "gross_mass": 1250.5,
      "net_mass": 1200.0,
      "house_consignment_sequence": 24,
      "packages": [{ "type": "CT", "quantity": 8, "shipping_marks": "MARK-002" }]
    }
  ]
}

T2 groupage note: house_consignment_sequence on each goods item links it to the matching house_consignments[].sequence_number. Sequence numbers are Navision/Qargo shipment line IDs — they are preserved as-is and do not need to be sequential from 1. AppSettings-sourced fields (Consignor, TransitPrincipal, guarantee GRN, authorisation C521, departure office, location of goods) are merged by the ingestion service — the external system only needs to send the shipment-variable fields.


Client resolution

The ingestion service resolves the client in this order:

  1. client_code — direct lookup by Client.Code (fastest; use when the external system knows the internal code)
  2. EORI match — looks up Client.IdentificationNumber against the relevant party EORI:
  3. IMparties.importer.eori
  4. EXparties.exporter.eori
  5. T1/T2parties.consignee.eori
  6. Auto-create — if no match is found, a new Client is created from the party details (name, eori, address) with IsVerified = false. Unverified clients are flagged in the UI for admin review.

API key management

Keys are managed via /Admin/ApiKeys (Beheerder role required):

  • Generate: enter a name (e.g. "Qargo Production") and optional expiry date → the raw key is displayed once and never stored (only the SHA-256 hash is kept)
  • Revoke: deactivates the key immediately; existing in-flight requests using the key will fail
  • Keys are scoped globally — any active key authenticates any POST /api/declarations request

Swagger UI (development)

Available at /swagger in Development and dv environments. Both authentication methods are pre-configured:

  • Bearer — paste an Azure AD access token (obtain via az account get-access-token or the MSAL flow)
  • ApiKey — paste a raw API key from /Admin/ApiKeys

In development, your existing web app browser session (cookie) also authenticates GET endpoints automatically — no token needed for read-only endpoints.