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.
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_sequenceon each goods item links it to the matchinghouse_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:
client_code— direct lookup byClient.Code(fastest; use when the external system knows the internal code)- EORI match — looks up
Client.IdentificationNumberagainst the relevant party EORI: IM→parties.importer.eoriEX→parties.exporter.eoriT1/T2→parties.consignee.eori- Auto-create — if no match is found, a new
Clientis created from the party details (name,eori, address) withIsVerified = 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/declarationsrequest
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-tokenor 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.