● Draft for integration planning · 3 November 2026

Screen a shipment with one API call

Send us the air waybill you already have. We screen every party and every goods line against sanctions, denied-party and export-control lists, verify the trading parties, and return one structured result.

Overview

One request covers up to four checks. Your account has a default set; you can choose per request.

CheckWhat it screensTypical time
denied_partyEvery party (shipper, consignee, agent, notify) against international sanctions lists, with politically exposed persons and enforcement lists where enabled.1–3 s
trade_regulationEvery goods line by HS code against the dual-use, military and sanctioned-goods control lists of the export country, with sanctions targeted at the destination country.1–3 s
kyc_aiShipper and consignee: AI-assisted verification against public sources. Does the company exist, does it match the declared name and address, is it active? Every finding comes with its sources.30–90 s
company_reportOptional add-on. Registered company data: status, legal form, incorporation date, employees, credit rating, recommended credit limit, shareholders.2–5 s

Quick start

Authenticate with your API key as a bearer token. Sandbox keys start with sk_test_ and never call a live source or generate charges. Live keys start with sk_live_. Keep keys on your server.

Check your connection
curl https://sandbox.qcargoapi.com/v1/account \
  -H "Authorization: Bearer sk_test_..."
Create a screening and wait up to 20 seconds for it
curl -X POST "https://sandbox.qcargoapi.com/v1/screenings?wait=20" \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d0f4a52-2f7e-4c1e-9a43-6b3c8f1d2e90" \
  -d @screening.json

How a screening runs

Denied-party and trade-regulation results are back within seconds. The AI verification researches public sources and takes longer. That is why a screening is asynchronous, and why you can read the fast results before the slow one is done.

1POST /screeningsYou send the FWB (and FHLs, if any) with your booking reference.
2202 AcceptedYou get the screening id at once, with the parsed shipment and any checks already finished.
3We run the checksEach check finishes on its own. GET /screenings/{id} shows progress.
4screening.completedOur webhook delivers the full, final result to your endpoint.

Prefer a single blocking call? Add ?wait= (up to 30 seconds). If everything finishes in time you get 200 OK with the final result; otherwise 202 with what is done so far, and the webhook follows.

What you send

Send the raw Cargo-IMP messages exactly as you have them. We read FWB (versions 16 and 17) and FHL (versions 4 and 5).

House before master. When you include FHL messages, we screen the parties and goods of each house waybill and take routing and flights from the FWB. Without FHLs we screen the FWB's own parties and goods. For consolidations, always send the FHLs: the real shipper and consignee are in the house. Each house then appears in shipment.houses, and every party and goods line carries the house_id it belongs to.
screening.json, with a fictional FWB message
{
  "reference": "CAI-BKG-2026-118842",
  "checks": [
    "denied_party",
    "trade_regulation",
    "kyc_ai"
  ],
  "input": {
    "format": "cargo_imp",
    "fwb": "FWB/16\n235-12345675AMSIST/T3K612.0MC3.10\nFLT/TK1958/14\nRTG/ISTTK\nSHP\nNAM/TULIPA PRECISION METALS BV\nADR/INDUSTRIEWEG 12\nLOC/EINDHOVEN\n/NL/5651GK/TE/31401234567\nCNE\nNAM/BOSPORUS TEKNIK MAKINA AS\nADR/ORGANIZE SANAYI BOLGESI 4 CAD NO 7\nLOC/ISTANBUL\n/TR/34906\nAGT//3747012/0001\n/NORTHDYKE FORWARDING BV\n/AMSTERDAM\nCVD/EUR//PP/NVD/NCV/XXX\nRTD/1/P2/K480.0/CQ/W480.0/R2.15/T1032.00\n/NG/MARAGING STEEL BARS\n/2/NH/72281050\nRTD/2/P1/K132.0/CQ/W132.0/R2.15/T283.80\n/NG/STEEL RETAINING RINGS\n/2/NH/73182900\nISU/03NOV26/AMSTERDAM"
  }
}

All companies, addresses and numbers in this guide are fictional.

Request fields

FieldDescription
referencerequiredYour own reference (booking or quote id). Returned unchanged and searchable.
checksoptionalWhich checks to run. Omit to use your account defaults.
input.formatrequiredcargo_imp for raw messages, or json for structured data (see the OpenAPI spec, ShipmentInput).
input.fwbrequired for cargo_impThe FWB message text, line breaks preserved.
input.fhloptionalArray of FHL message texts, one per house waybill.
metadataoptionalUp to 20 key/value strings, stored and returned unchanged.

What you get back

The screening object for the example above, once completed. The same object comes back from POST, from GET /screenings/{id} and in the webhook.

200 OK: completed screening
{
  "id": "scr_01JC4T8Z6Q2W9V3M7K5N0P1R2S",
  "object": "screening",
  "reference": "CAI-BKG-2026-118842",
  "environment": "sandbox",
  "status": "completed",
  "outcome": "review",
  "created_at": "2026-11-03T09:14:02Z",
  "completed_at": "2026-11-03T09:14:51Z",
  "metadata": {},
  "shipment": {
    "source": "fwb",
    "awb": "235-12345675",
    "houses": [],
    "origin": {
      "airport": "AMS",
      "country": "NL"
    },
    "destination": {
      "airport": "IST",
      "country": "TR"
    },
    "export_country": "NL",
    "flights": [
      {
        "flight": "TK1958",
        "date": "2026-11-14"
      }
    ],
    "parties": [
      {
        "id": "pty_1",
        "house_id": null,
        "role": "shipper",
        "name": "TULIPA PRECISION METALS BV",
        "type": "organisation",
        "address": {
          "street": "INDUSTRIEWEG 12",
          "city": "EINDHOVEN",
          "postal_code": "5651GK",
          "country": "NL"
        }
      },
      {
        "id": "pty_2",
        "house_id": null,
        "role": "consignee",
        "name": "BOSPORUS TEKNIK MAKINA AS",
        "type": "organisation",
        "address": {
          "street": "ORGANIZE SANAYI BOLGESI 4 CAD NO 7",
          "city": "ISTANBUL",
          "postal_code": "34906",
          "country": "TR"
        }
      },
      {
        "id": "pty_3",
        "house_id": null,
        "role": "agent",
        "name": "NORTHDYKE FORWARDING BV",
        "type": "organisation",
        "address": {
          "city": "AMSTERDAM",
          "country": "NL"
        }
      }
    ],
    "goods": [
      {
        "id": "gds_1",
        "house_id": null,
        "description": "MARAGING STEEL BARS",
        "hs_code": "72281050",
        "pieces": 2,
        "gross_weight_kg": 480.0
      },
      {
        "id": "gds_2",
        "house_id": null,
        "description": "STEEL RETAINING RINGS",
        "hs_code": "73182900",
        "pieces": 1,
        "gross_weight_kg": 132.0
      }
    ]
  },
  "warnings": [
    {
      "code": "party_address_incomplete",
      "message": "Agent has no street address; screened on name, city and country.",
      "path": "parties[2].address"
    }
  ],
  "checks": {
    "denied_party": {
      "status": "completed",
      "outcome": "clear",
      "completed_at": "2026-11-03T09:14:04Z",
      "error": null,
      "results": [
        {
          "party_id": "pty_1",
          "outcome": "clear",
          "matches": []
        },
        {
          "party_id": "pty_2",
          "outcome": "clear",
          "matches": []
        },
        {
          "party_id": "pty_3",
          "outcome": "clear",
          "matches": []
        }
      ]
    },
    "trade_regulation": {
      "status": "completed",
      "outcome": "review",
      "completed_at": "2026-11-03T09:14:05Z",
      "error": null,
      "jurisdiction": "EU",
      "sanctions_target": "TR",
      "results": [
        {
          "goods_id": "gds_1",
          "hs_code": "72281050",
          "outcome": "review",
          "regimes": [
            {
              "regime": "dual_use",
              "result": "match"
            },
            {
              "regime": "military",
              "result": "no_match"
            },
            {
              "regime": "sanctions",
              "result": "not_applicable"
            }
          ],
          "controlled_items": [
            {
              "regime": "dual_use",
              "control_number": "1C216",
              "hs_code": "72281050",
              "category": "1 - Special materials and related equipment / C - Materials",
              "description": "Maraging steel, other than that specified in 1C116, 'capable of' an ultimate tensile strength of 1 950 MPa or more, at 293 K (20 °C). Note: 1C216 does not control forms in which all linear dimensions are 75 mm or less. Technical note: the phrase maraging steel 'capable of' encompasses maraging steel before or after heat treatment.",
              "control_list": {
                "name": "Regulation (EU) 2021/821 — Union regime for the control of exports, brokering, technical assistance, transit and transfer of dual-use items",
                "reference": "02021R0821-20251115",
                "publisher": "European Union",
                "url": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02021R0821-20251115"
              },
              "note": "The match is a correlation via the HS code. The goods fall under 1C216 only if they meet its technical specification; an export licence is then required."
            }
          ]
        },
        {
          "goods_id": "gds_2",
          "hs_code": "73182900",
          "outcome": "clear",
          "regimes": [
            {
              "regime": "dual_use",
              "result": "no_match"
            },
            {
              "regime": "military",
              "result": "no_match"
            },
            {
              "regime": "sanctions",
              "result": "not_applicable"
            }
          ],
          "controlled_items": []
        }
      ]
    },
    "kyc_ai": {
      "status": "completed",
      "outcome": "review",
      "completed_at": "2026-11-03T09:14:51Z",
      "error": null,
      "results": [
        {
          "party_id": "pty_1",
          "outcome": "clear",
          "confidence": 96,
          "summary": "Registered Dutch private company at the declared address, active, manufacturer of precision metal parts.",
          "findings": [
            {
              "field": "name",
              "declared": "TULIPA PRECISION METALS BV",
              "found": "Tulipa Precision Metals B.V.",
              "verdict": "confirmed",
              "confidence": 98,
              "explanation": "Company register lists the same legal name; capitalisation and punctuation differ only.",
              "sources": [
                "https://register.example/nl/tulipa-precision-metals"
              ]
            },
            {
              "field": "street",
              "declared": "INDUSTRIEWEG 12",
              "found": "Industrieweg 12",
              "verdict": "confirmed",
              "confidence": 95,
              "explanation": "Registered office address matches.",
              "sources": [
                "https://register.example/nl/tulipa-precision-metals"
              ]
            },
            {
              "field": "activity_status",
              "declared": null,
              "found": "active",
              "verdict": "confirmed",
              "confidence": 94,
              "explanation": "Registration is active; no dissolution or bankruptcy notice found.",
              "sources": [
                "https://register.example/nl/tulipa-precision-metals"
              ]
            }
          ]
        },
        {
          "party_id": "pty_2",
          "outcome": "review",
          "confidence": 71,
          "summary": "Company found in Istanbul with a matching name, but the registered postal code differs from the declared one.",
          "findings": [
            {
              "field": "name",
              "declared": "BOSPORUS TEKNIK MAKINA AS",
              "found": "Bosporus Teknik Makina A.Ş.",
              "verdict": "confirmed",
              "confidence": 93,
              "explanation": "Trade registry gazette lists the company under this name.",
              "sources": [
                "https://gazette.example/tr/bosporus-teknik"
              ]
            },
            {
              "field": "postal_code",
              "declared": "34906",
              "found": "34953",
              "verdict": "differs",
              "confidence": 80,
              "explanation": "The trade registry and the company website both give 34953 (Tuzla organised industrial zone).",
              "sources": [
                "https://gazette.example/tr/bosporus-teknik",
                "https://bosporus-teknik.example/contact"
              ]
            }
          ]
        }
      ]
    }
  },
  "usage": {
    "billable_checks": 3,
    "items": [
      {
        "check": "denied_party",
        "units": 3
      },
      {
        "check": "trade_regulation",
        "units": 4
      },
      {
        "check": "kyc_ai",
        "units": 2
      }
    ]
  }
}

While the AI verification is still running, the same call returns this:

202 Accepted: still processing
{
  "id": "scr_01JC4T8Z6Q2W9V3M7K5N0P1R2S",
  "object": "screening",
  "reference": "CAI-BKG-2026-118842",
  "environment": "sandbox",
  "status": "processing",
  "outcome": null,
  "created_at": "2026-11-03T09:14:02Z",
  "completed_at": null,
  "checks": {
    "denied_party": {
      "status": "completed",
      "outcome": "clear",
      "completed_at": "2026-11-03T09:14:04Z",
      "error": null
    },
    "trade_regulation": {
      "status": "completed",
      "outcome": "review",
      "completed_at": "2026-11-03T09:14:05Z",
      "error": null
    },
    "kyc_ai": {
      "status": "processing",
      "outcome": null,
      "completed_at": null,
      "error": null
    }
  }
}

Reading the result

Start with the top-level outcome. It is the most severe outcome of all checks.

Clear clear

Nothing found that needs attention. The shipment can proceed as far as these checks are concerned.

Review review

At least one possible match or controlled item. A person should look before the shipment proceeds. The check results say exactly what and why.

Incomplete incomplete

A check, or part of one, could not complete (its error says why). Nothing was found in what did complete. Failed parts are not billed. To try again, create a new screening with a new Idempotency-Key, optionally with retry_of pointing to this one.

Warnings

warnings[] flag gaps in the message itself, such as a missing HS code or an incomplete address. They do not stop the screening, but they do limit what can be checked.

In the example

Status values

LevelValues
Screening statusprocessing, completed
Check statusqueued, processing, completed, failed, skipped (the input lacked what the check needs, such as an HS code)
Trade-regulation regimes[].resultmatch, no_match, not_applicable (no such regime for this jurisdiction or destination)
KYC findings[].verdictconfirmed, differs, not_found

Webhooks

We deliver screening.completed to your webhook URL once every requested check is final. The body is { id, type, created_at, data }, where data is the full screening object.

Verifying the signature (Node.js)
import crypto from "node:crypto";

// header: "t=1793710800,v1=5f2b8c..."   rawBody: the request body exactly as received
export function verify(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // older than 5 minutes
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Errors, retries, idempotency

Every POST /screenings needs an Idempotency-Key. Use one per screening you mean to create, such as a UUID or your booking id plus a revision. If a request times out or you get a 5xx, retry with the same key. You get the original screening back, and it is never created or billed twice.

HTTPerror.codeMeaning
400invalid_requestMissing or malformed field; param names it.
401unauthenticatedMissing or unknown API key.
403check_not_enabled, account_inactiveThe key is valid but not allowed to do this.
409idempotency_conflictThe key was already used with a different body.
422no_screenable_contentThe message parsed, but has nothing to screen (for example no parties). Not billed.
429rate_limitedSlow down; wait the Retry-After seconds.
5xxserver_errorOur side. Safe to retry with the same Idempotency-Key.

Errors always have the shape {"error": {"code", "message", "param", "request_id"}}. Quote the request_id when you contact us.

Sandbox

Sandbox results are deterministic, so you can test every branch of your integration:

Put this in your requestYou get
Party name containing SANDBOX DENIEDA denied-party match on that party
HS code starting with 7228Dual-use item 1C216, as in the example. Any other HS code gives no match.
Party name containing SANDBOX UNKNOWNKYC findings not_found, outcome review
Party name containing SANDBOX TIMEOUTThe checks for that party fail with source_timeout, and the screening outcome is incomplete

What is billed

Only checks that complete are billed. Failed and skipped checks, 422 responses and idempotent retries are not billed. The usage block on every screening shows what it counts towards your monthly invoice. Units follow the screening requests actually made: one per party for denied_party, one per goods line per control regime for trade_regulation, and one per verified party for kyc_ai and company_report. One waybill can therefore count several units; the example above counts 3 + 4 + 2. Failed parts are not counted. Your contract is leading.

OpenAPI specification

The full contract, with every field, enum and error, is available as OpenAPI 3.1. Load it into Swagger UI, Postman or your code generator.

openapi.yaml
openapi: 3.1.0
info:
  title: Qcargo Screening API
  version: 1.0.0-draft.1
  summary: Screen air cargo shipments and trading parties in one call.
  license:
    name: Proprietary, provided under the client's service agreement
  description: |
    Send a shipment (a Cargo-IMP FWB/FHL message, or structured JSON) and receive one consolidated
    screening result: denied-party screening of every party, trade-regulation screening of every goods
    line (dual-use, military and sanctioned goods), and AI-assisted verification of the trading parties.

    **Draft for integration planning.** The product name and some field details are
    provisional and may change before the first live release. Changes will be announced and
    reflected in an updated version of this document.

    ### Conventions
    * All requests and responses are JSON (`Content-Type: application/json`), UTF-8.
    * Timestamps are ISO 8601 in UTC. Country codes are ISO 3166-1 alpha-2. Airport codes are IATA.
    * Unknown fields in responses must be ignored by the client: new fields are added without a version change.
    * Identifiers are opaque strings with a type prefix (`scr_` screening, `pty_` party, `gds_` goods line, `evt_` event).

    ### Sandbox test triggers
    Sandbox keys never call a live source. Results are deterministic so you can test every branch:
    * a party name containing `SANDBOX DENIED` returns a denied-party match;
    * an HS code starting with `7228` returns the dual-use item 1C216, any other code returns no match;
    * a party name containing `SANDBOX UNKNOWN` returns a KYC result with `not_found` findings and outcome `review`;
    * a party name containing `SANDBOX TIMEOUT` makes the checks for that party fail with `source_timeout`.
servers:
  - url: https://sandbox.qcargoapi.com/v1
    description: Sandbox. Test keys only, deterministic results, never billed.
  - url: https://qcargoapi.com/v1
    description: Production. Live keys only.
security:
  - apiKey: []
tags:
  - name: Screenings
    description: Submit shipments and retrieve screening results.
  - name: Account
    description: Your account configuration.
  - name: Webhooks
    description: Events we send to your endpoint.
paths:
  /screenings:
    post:
      tags: [Screenings]
      operationId: createScreening
      summary: Create a screening
      description: |
        Submits a shipment for screening. The response is the screening object.

        Denied-party and trade-regulation checks typically finish within a few seconds. The AI party
        verification researches public sources and can take 30–90 seconds. Screening is therefore
        asynchronous:

        * By default the call returns immediately with `202 Accepted` and `status: processing`.
        * Pass `wait` (seconds, max 30) to hold the connection until the screening completes or the wait
          expires. If everything finished in time you get `200 OK` with `status: completed`; otherwise
          `202` with whatever checks already completed.
        * Get the final result with `GET /screenings/{id}` or, preferably, a `screening.completed` webhook.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: wait
          in: query
          required: false
          description: Seconds to wait for completion before responding (0–30). Default 0.
          schema: { type: integer, minimum: 0, maximum: 30, default: 0 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScreeningRequest' }
            examples:
              cargoImp:
                summary: Cargo-IMP FWB message
                value:
                  reference: CAI-BKG-2026-118842
                  checks: [denied_party, trade_regulation, kyc_ai]
                  input:
                    format: cargo_imp
                    fwb: |-
                      FWB/16
                      235-12345675AMSIST/T3K612.0MC3.10
                      FLT/TK1958/14
                      RTG/ISTTK
                      SHP
                      NAM/TULIPA PRECISION METALS BV
                      ADR/INDUSTRIEWEG 12
                      LOC/EINDHOVEN
                      /NL/5651GK/TE/31401234567
                      CNE
                      NAM/BOSPORUS TEKNIK MAKINA AS
                      ADR/ORGANIZE SANAYI BOLGESI 4 CAD NO 7
                      LOC/ISTANBUL
                      /TR/34906
                      AGT//3747012/0001
                      /NORTHDYKE FORWARDING BV
                      /AMSTERDAM
                      CVD/EUR//PP/NVD/NCV/XXX
                      RTD/1/P2/K480.0/CQ/W480.0/R2.15/T1032.00
                      /NG/MARAGING STEEL BARS
                      /2/NH/72281050
                      RTD/2/P1/K132.0/CQ/W132.0/R2.15/T283.80
                      /NG/STEEL RETAINING RINGS
                      /2/NH/73182900
                      ISU/03NOV26/AMSTERDAM
              json:
                summary: Structured JSON
                value:
                  reference: CAI-BKG-2026-118842
                  checks: [denied_party, trade_regulation]
                  input:
                    format: json
                    shipment:
                      awb: 235-12345675
                      origin: { airport: AMS, country: NL }
                      destination: { airport: IST, country: TR }
                      flights: [{ flight: TK1958, date: '2026-11-14' }]
                      parties:
                        - role: shipper
                          name: TULIPA PRECISION METALS BV
                          address: { street: INDUSTRIEWEG 12, city: EINDHOVEN, postal_code: 5651GK, country: NL }
                        - role: consignee
                          name: BOSPORUS TEKNIK MAKINA AS
                          address: { street: ORGANIZE SANAYI BOLGESI 4 CAD NO 7, city: ISTANBUL, postal_code: '34906', country: TR }
                      goods:
                        - description: MARAGING STEEL BARS
                          hs_code: '72281050'
                          pieces: 2
                          gross_weight_kg: 480.0
      responses:
        '200':
          description: Screening completed within the requested wait.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Screening' }
              examples:
                completed: { $ref: '#/components/examples/ScreeningCompleted' }
        '202':
          description: Screening accepted and still processing.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Screening' }
              examples:
                processing: { $ref: '#/components/examples/ScreeningProcessing' }
        '400': { $ref: '#/components/responses/InvalidRequest' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '422': { $ref: '#/components/responses/UnprocessableMessage' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/ServerError' }
    get:
      tags: [Screenings]
      operationId: listScreenings
      summary: List screenings
      description: Returns your screenings, newest first, with cursor pagination.
      parameters:
        - { name: reference, in: query, schema: { type: string }, description: Filter on your own reference. }
        - { name: awb, in: query, schema: { type: string }, description: 'Filter on AWB number, e.g. 235-12345675.' }
        - { name: status, in: query, schema: { $ref: '#/components/schemas/ScreeningStatus' } }
        - { name: outcome, in: query, schema: { $ref: '#/components/schemas/Outcome' } }
        - { name: created_after, in: query, schema: { type: string, format: date-time } }
        - { name: created_before, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string }, description: '`next_cursor` from the previous page.' }
      responses:
        '200':
          description: A page of screenings.
          content:
            application/json:
              schema:
                type: object
                required: [data, has_more]
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Screening' } }
                  has_more: { type: boolean }
                  next_cursor: { type: [string, 'null'] }
        '401': { $ref: '#/components/responses/Unauthenticated' }
  /screenings/{id}:
    get:
      tags: [Screenings]
      operationId: getScreening
      summary: Retrieve a screening
      parameters:
        - { name: id, in: path, required: true, schema: { type: string }, example: scr_01JC4T8Z6Q2W9V3M7K5N0P1R2S }
      responses:
        '200':
          description: The screening.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Screening' }
              examples:
                completed: { $ref: '#/components/examples/ScreeningCompleted' }
        '401': { $ref: '#/components/responses/Unauthenticated' }
        '404': { $ref: '#/components/responses/NotFound' }
  /account:
    get:
      tags: [Account]
      operationId: getAccount
      summary: Retrieve your account
      description: The checks enabled for your account and the environment of the key you used. Useful as a connectivity test.
      responses:
        '200':
          description: Your account.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Account' }
              example:
                object: account
                id: acc_cargoai
                name: Cargo.AI
                environment: sandbox
                enabled_checks: [denied_party, trade_regulation, kyc_ai, company_report]
                default_checks: [denied_party, trade_regulation, kyc_ai]
                input_formats: [cargo_imp, json]
                webhook_url: https://hooks.cargoai.example/screening
        '401': { $ref: '#/components/responses/Unauthenticated' }
webhooks:
  screening.completed:
    post:
      tags: [Webhooks]
      operationId: screeningCompleted
      summary: A screening finished
      description: |
        Sent once every requested check has reached a final state (`completed`, `failed` or `skipped`).
        The payload carries the full screening object.

        **Verifying the signature.** Every delivery has a `Screening-Signature` header:
        `t=<unix timestamp>,v1=<hex HMAC-SHA256>`. Compute HMAC-SHA256 over `"<t>.<raw request body>"`
        with your webhook signing secret and compare it to `v1` in constant time. Reject deliveries whose
        timestamp is more than 5 minutes old.

        **Delivery.** Respond with any 2xx within 10 seconds. Failed deliveries are retried with
        exponential backoff for up to 24 hours. Deliveries can arrive more than once: deduplicate on `id`.
        Every delivery is sent with the header `User-Agent: Qcargo-Screening/1`.
      parameters:
        - name: Screening-Signature
          in: header
          required: true
          schema: { type: string }
          example: t=1793710800,v1=5f2b8c0e6a7d4b1f9e3c2a8d7b6e5f4a3c2b1d0e9f8a7b6c5d4e3f2a1b0c9d8e
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookEvent' }
      responses:
        '200': { description: Acknowledged. }
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer <api key>`. Sandbox keys start with `sk_test_`, live keys with `sk_live_`.
        Keys are issued per client; keep them server-side.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        A unique value per screening you intend to create (a UUID, or your booking id plus a revision).
        Retrying with the same key within 24 hours returns the original screening instead of creating
        and billing a second one. Reusing a key with a different body returns `409`.
      schema: { type: string, maxLength: 255 }
      example: 7d0f4a52-2f7e-4c1e-9a43-6b3c8f1d2e90
  schemas:
    CheckType:
      type: string
      description: |
        * `denied_party` — every party against international sanctions lists, with politically exposed persons and enforcement lists where enabled for your account.
        * `trade_regulation` — every goods line (HS code) against dual-use, military and sanctioned-goods
          control lists of the export jurisdiction, with sanctions targeted at the destination country.
        * `kyc_ai` — AI-assisted verification of shipper and consignee against public sources: does the
          company exist and match the declared name and address, is it active, which sources confirm it.
        * `company_report` — (optional add-on) registered company data: status, legal form,
          incorporation date, employees, credit rating and recommended credit limit, shareholders.
      enum: [denied_party, trade_regulation, kyc_ai, company_report]
    ScreeningRequest:
      type: object
      required: [reference, input]
      properties:
        reference:
          type: string
          maxLength: 120
          description: Your own reference (booking id, quote id). Returned unchanged and searchable.
        checks:
          type: array
          items: { $ref: '#/components/schemas/CheckType' }
          uniqueItems: true
          description: Checks to run. Omit to run your account's default checks.
        retry_of:
          type: string
          description: Optional id of an earlier screening this one repeats (for example after a check failed). Only links the two; it does not change billing.
        metadata:
          type: object
          additionalProperties: { type: string, maxLength: 500 }
          maxProperties: 20
          description: Free key/value pairs stored with the screening and returned unchanged.
        input:
          oneOf:
            - $ref: '#/components/schemas/CargoImpInput'
            - $ref: '#/components/schemas/JsonInput'
          discriminator:
            propertyName: format
            mapping:
              cargo_imp: '#/components/schemas/CargoImpInput'
              json: '#/components/schemas/JsonInput'
    CargoImpInput:
      type: object
      required: [format, fwb]
      description: |
        Raw IATA Cargo-IMP messages. Supported: FWB (versions 16 and 17) and FHL (version 4 and 5).
        **House before master:** when one or more FHL messages are supplied, the parties and goods of each
        house are screened; the FWB then supplies routing and flights. Without FHL the FWB parties and
        goods are screened. HS codes are read from RTD `/NH/` lines and OCI entries (FWB) and `HTS/` lines (FHL).
        An FHL house without `HTS/` codes takes its HS codes from its OCI entries.
        An FHL may be a consolidation list: one message with several HBS houses is accepted, and each
        house is screened. The FWB agent has no country line; it gets the country of the origin
        airport when that is known.
      properties:
        format: { type: string, const: cargo_imp }
        fwb: { type: string, description: 'The FWB message text, line breaks preserved.' }
        fhl:
          type: array
          items: { type: string }
          description: Zero or more FHL message texts, one per house air waybill.
    JsonInput:
      type: object
      required: [format, shipment]
      properties:
        format: { type: string, const: json }
        shipment: { $ref: '#/components/schemas/ShipmentInput' }
    ShipmentInput:
      type: object
      required: [parties]
      properties:
        awb: { type: string, description: 'Master AWB, e.g. 235-12345675.' }
        hawb: { type: string }
        origin: { $ref: '#/components/schemas/Place' }
        destination: { $ref: '#/components/schemas/Place' }
        export_country:
          type: string
          description: ISO country whose export rules apply. Defaults to the origin country.
        flights:
          type: array
          items:
            type: object
            properties:
              flight: { type: string, example: TK1958 }
              date: { type: string, format: date }
        parties:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/PartyInput' }
        goods:
          type: array
          items: { $ref: '#/components/schemas/GoodsInput' }
          description: Required for `trade_regulation`.
    Place:
      type: object
      properties:
        airport: { type: string, description: IATA airport code. }
        country: { type: string, description: ISO 3166-1 alpha-2. }
    PartyRole:
      type: string
      enum: [shipper, consignee, agent, notify, end_user, other]
    PartyInput:
      type: object
      required: [role, name]
      properties:
        role: { $ref: '#/components/schemas/PartyRole' }
        name: { type: string }
        address: { $ref: '#/components/schemas/Address' }
        registration_number: { type: string, description: 'Chamber of Commerce / company register number, if known. Used to pick the right company for kyc_ai and company_report.' }
        vat_number: { type: string }
        type:
          type: string
          enum: [organisation, individual]
          default: organisation
    Address:
      type: object
      properties:
        street: { type: string }
        city: { type: string }
        postal_code: { type: string }
        region: { type: string }
        country: { type: string, description: ISO 3166-1 alpha-2. }
    GoodsInput:
      type: object
      required: [description]
      properties:
        description: { type: string }
        hs_code:
          type: string
          description: 6 to 14 digits; dots and spaces are ignored. At least 6 digits are needed for trade-regulation screening.
        pieces: { type: integer }
        gross_weight_kg: { type: number }
        cas_number: { type: string, description: 'CAS registry number for chemical substances, e.g. 7789-29-9.' }
    ScreeningStatus:
      type: string
      enum: [processing, completed]
      description: '`completed` once every requested check is final (completed, failed or skipped).'
    Outcome:
      type: string
      description: |
        * `clear` — nothing found that needs attention.
        * `review` — at least one possible match or controlled item: a person should look before the shipment proceeds.
        * `incomplete` — at least one check, or part of one, could not be completed (see `error`), a requested check was `skipped`, or part of the shipment was not screened (for example a house without shipper or consignee in a consolidation, see the `fhl_without_parties` warning); nothing was found in what did complete. To try again, create a new screening with a new `Idempotency-Key` (optionally `retry_of`); failed parts were not billed.
      enum: [clear, review, incomplete]
    CheckStatus:
      type: string
      enum: [queued, processing, completed, failed, skipped]
      description: |
        * `completed` — every part of the check finished, or some parts finished and the others carry an `error` in their result.
        * `failed` — no part of the check could be completed.
        * `skipped` — the input did not contain what the check needs (for example no HS code). The check has no outcome of its own, but the screening's outcome is then at least `incomplete`.
        Failed and skipped checks are not billed.
    Screening:
      type: object
      required: [id, object, reference, environment, status, created_at, checks]
      properties:
        id: { type: string, example: scr_01JC4T8Z6Q2W9V3M7K5N0P1R2S }
        object: { type: string, const: screening }
        reference: { type: string }
        environment: { type: string, enum: [sandbox, live] }
        status: { $ref: '#/components/schemas/ScreeningStatus' }
        outcome:
          oneOf: [{ $ref: '#/components/schemas/Outcome' }, { type: 'null' }]
          description: Overall outcome, the most severe of all checks. `null` while processing.
        created_at: { type: string, format: date-time }
        completed_at: { type: [string, 'null'], format: date-time }
        metadata: { type: object, additionalProperties: { type: string } }
        shipment: { $ref: '#/components/schemas/ParsedShipment' }
        warnings:
          type: array
          items: { $ref: '#/components/schemas/Warning' }
        checks:
          type: object
          description: One entry per requested check.
          properties:
            denied_party: { $ref: '#/components/schemas/DeniedPartyCheck' }
            trade_regulation: { $ref: '#/components/schemas/TradeRegulationCheck' }
            kyc_ai: { $ref: '#/components/schemas/KycAiCheck' }
            company_report: { $ref: '#/components/schemas/CompanyReportCheck' }
        usage:
          type: object
          description: What this screening counts towards your invoice. Final once `status` is `completed`.
          properties:
            billable_checks: { type: integer }
            items:
              type: array
              items:
                type: object
                properties:
                  check: { $ref: '#/components/schemas/CheckType' }
                  units: { type: integer, description: 'Billing units as agreed in your contract. Units follow the screening requests actually made: denied_party one per party screened; trade_regulation one per goods line per control regime screened; kyc_ai and company_report one per party verified. One waybill can therefore count several units. Failed parts are not counted.' }
    ParsedShipment:
      type: object
      description: The shipment as the platform read it. Use it to confirm the message was interpreted as intended.
      properties:
        source: { type: string, enum: [fwb, fhl, json], description: Where the screened parties and goods came from. }
        awb: { type: [string, 'null'] }
        houses:
          type: array
          description: The house waybills read from the FHL messages. Empty when only an FWB was sent; parties and goods then belong to the master.
          items:
            type: object
            properties:
              id: { type: string, example: hse_1 }
              hawb: { type: string }
        origin: { $ref: '#/components/schemas/Place' }
        destination: { $ref: '#/components/schemas/Place' }
        export_country: { type: string }
        flights:
          type: array
          items: { type: object, properties: { flight: { type: string }, date: { type: string, format: date } } }
        parties:
          type: array
          items:
            allOf:
              - type: object
                required: [id]
                properties:
                  id: { type: string, example: pty_1 }
                  house_id: { type: [string, 'null'], description: The house this party belongs to; null for the master. }
              - $ref: '#/components/schemas/PartyInput'
        goods:
          type: array
          items:
            allOf:
              - type: object
                required: [id]
                properties:
                  id: { type: string, example: gds_1 }
                  house_id: { type: [string, 'null'], description: The house this goods line belongs to; null for the master. }
              - $ref: '#/components/schemas/GoodsInput'
    Warning:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable. New codes may be added.
          examples:
            - hs_code_missing
            - hs_code_too_short
            - oci_hs_code_unassigned
            - goods_description_missing
            - party_address_incomplete
            - party_country_missing
            - party_without_name
            - country_inferred_from_party
            - fhl_without_parties
            - fhl_duplicate_house
            - unknown_segment_ignored
        message: { type: string }
        path: { type: string, description: 'Where in the parsed shipment, e.g. goods[1] or parties[2].address.' }
    CheckBase:
      type: object
      required: [status]
      properties:
        status: { $ref: '#/components/schemas/CheckStatus' }
        outcome:
          oneOf: [{ $ref: '#/components/schemas/Outcome' }, { type: 'null' }]
        completed_at: { type: [string, 'null'], format: date-time }
        error:
          oneOf: [{ $ref: '#/components/schemas/CheckError' }, { type: 'null' }]
    CheckError:
      type: object
      properties:
        code:
          type: string
          description: |
            Machine-readable. New codes may be added. `input_insufficient`: the input lacks what this
            part needs (for example a goods line without an HS code), or the part is not screened, as
            for individuals in denied-party screening. `results_partial`: matches are returned, but not
            every possible match could be retrieved in full.
          examples: [source_unavailable, source_timeout, input_insufficient, results_partial]
        message: { type: string }
    DeniedPartyCheck:
      allOf:
        - $ref: '#/components/schemas/CheckBase'
        - type: object
          properties:
            results:
              type: array
              items:
                type: object
                properties:
                  party_id: { type: string }
                  outcome: { $ref: '#/components/schemas/Outcome' }
                  error:
                    oneOf: [{ $ref: '#/components/schemas/CheckError' }, { type: 'null' }]
                    description: Set when this part could not be completed; the rest of the check still stands.
                  matches:
                    type: array
                    description: Possible matches at or above the screening threshold. Matches below the threshold are not returned. Empty when nothing was found.
                    items:
                      type: object
                      properties:
                        matched_name: { type: string }
                        aliases: { type: array, items: { type: string }, description: Other names the listed entity is known by. }
                        score: { type: integer, minimum: 0, maximum: 100, description: Match strength. }
                        entity_type: { type: string, enum: [organisation, individual, unknown] }
                        countries: { type: array, items: { type: string }, description: ISO 3166-1 alpha-2 codes associated with the listed entity. }
                        categories:
                          type: array
                          description: Why this entity is listed. One match can carry several categories.
                          items: { type: string, enum: [sanctions, former_sanctions, pep, enforcement, adverse_media, state_owned, insolvency, other] }
                        sanctions:
                          type: array
                          description: One entry per sanction listing. Empty for matches that are not sanctions related.
                          items:
                            type: object
                            properties:
                              authority: { type: [string, 'null'], example: European Union }
                              programme: { type: [string, 'null'], example: Russia }
                              measures: { type: array, items: { type: string }, example: [Asset freeze] }
                              status: { type: string, enum: [current, former] }
                        sources:
                          type: array
                          description: Public sources for the listing. Can be empty.
                          items:
                            type: object
                            properties:
                              title: { type: [string, 'null'] }
                              url: { type: string }
    TradeRegulationCheck:
      allOf:
        - $ref: '#/components/schemas/CheckBase'
        - type: object
          properties:
            jurisdiction: { type: string, description: 'Export jurisdiction whose rules were applied, e.g. EU.' }
            sanctions_target: { type: string, description: Destination country used for sanctioned-goods screening. }
            results:
              type: array
              items:
                type: object
                properties:
                  goods_id: { type: string }
                  hs_code: { type: string }
                  outcome: { $ref: '#/components/schemas/Outcome' }
                  error:
                    oneOf: [{ $ref: '#/components/schemas/CheckError' }, { type: 'null' }]
                    description: Set when this part could not be completed; the rest of the check still stands.
                  regimes:
                    type: array
                    description: Each control regime screened for this goods line.
                    items:
                      type: object
                      properties:
                        regime: { type: string, enum: [dual_use, military, sanctions] }
                        result:
                          type: string
                          enum: [no_match, match, not_applicable, not_covered]
                          description: |
                            * `not_applicable` — no such regime exists for this jurisdiction or destination (for example no sanctions programme targets the destination).
                            * `not_covered` — this regime could not be screened for this jurisdiction. The goods line's outcome is then `incomplete`.
                  controlled_items:
                    type: array
                    items: { $ref: '#/components/schemas/ControlledItem' }
    ControlledItem:
      type: object
      properties:
        regime: { type: string, enum: [dual_use, military, sanctions] }
        control_number: { type: string, description: Export control classification number or sanctioned item number, example: 1C216 }
        hs_code: { type: string, description: The customs code (prefix) the item is correlated with. Can be shorter than the declared HS code. }
        cas_number: { type: [string, 'null'] }
        description: { type: [string, 'null'] }
        category: { type: [string, 'null'] }
        control_list:
          type: [object, 'null']
          properties:
            name: { type: string }
            reference: { type: [string, 'null'] }
            publisher: { type: [string, 'null'] }
            url: { type: [string, 'null'] }
        note: { type: string, description: 'The match is a correlation via the HS code: it means the goods MAY fall under this entry. Whether they do depends on their technical specifications.' }
    KycAiCheck:
      allOf:
        - $ref: '#/components/schemas/CheckBase'
        - type: object
          properties:
            results:
              type: array
              items:
                type: object
                properties:
                  party_id: { type: string }
                  outcome: { $ref: '#/components/schemas/Outcome' }
                  error:
                    oneOf: [{ $ref: '#/components/schemas/CheckError' }, { type: 'null' }]
                    description: Set when this part could not be completed; the rest of the check still stands.
                  confidence: { type: integer, minimum: 0, maximum: 100, description: Overall confidence that the party is who it claims to be. }
                  summary: { type: string }
                  findings:
                    type: array
                    items:
                      type: object
                      properties:
                        field: { type: string, enum: [name, street, city, postal_code, country, registration_number, activity_status, website] }
                        declared: { type: [string, 'null'] }
                        found: { type: [string, 'null'] }
                        verdict: { type: string, enum: [confirmed, differs, not_found] }
                        confidence: { type: integer, minimum: 0, maximum: 100 }
                        explanation: { type: string }
                        sources: { type: array, items: { type: string, format: uri } }
    CompanyReportCheck:
      allOf:
        - $ref: '#/components/schemas/CheckBase'
        - type: object
          properties:
            results:
              type: array
              items:
                type: object
                properties:
                  party_id: { type: string }
                  outcome: { $ref: '#/components/schemas/Outcome' }
                  error:
                    oneOf: [{ $ref: '#/components/schemas/CheckError' }, { type: 'null' }]
                    description: Set when this part could not be completed; the rest of the check still stands.
                  company:
                    type: object
                    properties:
                      registered_name: { type: [string, 'null'] }
                      registration_number: { type: [string, 'null'] }
                      status: { type: string, enum: [active, inactive, pending, unknown] }
                      status_detail: { type: [string, 'null'], description: 'The local status wording, e.g. "In liquidation" or "Dissolved".' }
                      legal_form: { type: [string, 'null'] }
                      incorporated_on: { type: [string, 'null'], format: date }
                      employees: { type: [integer, 'null'], description: Most recently reported number of employees. }
                      credit_rating: { type: [string, 'null'], enum: [A, B, C, D, E, null], description: 'A (lowest risk) to E (highest risk); null when not rated.' }
                      probability_of_default: { type: [number, 'null'], description: 'Estimated probability of default within 12 months, in percent.' }
                      recommended_credit_limit: { type: [object, 'null'], properties: { amount: { type: number }, currency: { type: string } } }
                      shareholders:
                        type: array
                        description: Current shareholders. Can be empty, depending on the country of registration.
                        items:
                          type: object
                          properties:
                            name: { type: string }
                            type: { type: string, enum: [person, company, other] }
                            share_percentage: { type: [number, 'null'] }
    Account:
      type: object
      properties:
        object: { type: string, const: account }
        id: { type: string }
        name: { type: string }
        environment: { type: string, enum: [sandbox, live] }
        enabled_checks: { type: array, items: { $ref: '#/components/schemas/CheckType' } }
        default_checks: { type: array, items: { $ref: '#/components/schemas/CheckType' } }
        input_formats: { type: array, items: { type: string, enum: [cargo_imp, json] } }
        webhook_url: { type: [string, 'null'] }
    WebhookEvent:
      type: object
      required: [id, type, created_at, data]
      properties:
        id: { type: string, example: evt_01JC4TB2M8X7Y6Z5A4B3C2D1E0 }
        type: { type: string, const: screening.completed }
        created_at: { type: string, format: date-time }
        data: { $ref: '#/components/schemas/Screening' }
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            param: { type: [string, 'null'], description: The request field the error refers to. }
            request_id: { type: string, description: Quote this when contacting support. }
  responses:
    InvalidRequest:
      description: The request is malformed (missing field, wrong type, unknown check).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: invalid_request, message: "'reference' is required.", param: reference, request_id: req_8f3a2c } }
    Unauthenticated:
      description: Missing or unknown API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: unauthenticated, message: Invalid API key., param: null, request_id: req_8f3a2d } }
    Forbidden:
      description: The key is valid but not allowed to do this (account inactive, check not enabled, sandbox key on production).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: check_not_enabled, message: "'company_report' is not enabled for your account.", param: checks, request_id: req_8f3a2e } }
    NotFound:
      description: No screening with this id for your account.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    IdempotencyConflict:
      description: The Idempotency-Key was already used with a different request body.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: idempotency_conflict, message: This Idempotency-Key was used with a different request., param: Idempotency-Key, request_id: req_8f3a2f } }
    UnprocessableMessage:
      description: The message could be read as JSON but contains nothing screenable (for example an FWB without any party). Not billed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { error: { code: no_screenable_content, message: 'No shipper, consignee or agent found in the FWB message.', param: input.fwb, request_id: req_8f3a30 } }
    RateLimited:
      description: Too many requests. Retry after the number of seconds in the `Retry-After` header.
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ServerError:
      description: Something went wrong on our side. Safe to retry with the same Idempotency-Key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  examples:
    ScreeningProcessing:
      summary: Accepted, fast checks done, AI verification still running
      value:
        id: scr_01JC4T8Z6Q2W9V3M7K5N0P1R2S
        object: screening
        reference: CAI-BKG-2026-118842
        environment: sandbox
        status: processing
        outcome: null
        created_at: '2026-11-03T09:14:02Z'
        completed_at: null
        checks:
          denied_party: { status: completed, outcome: clear, completed_at: '2026-11-03T09:14:04Z', error: null }
          trade_regulation: { status: completed, outcome: review, completed_at: '2026-11-03T09:14:05Z', error: null }
          kyc_ai: { status: processing, outcome: null, completed_at: null, error: null }
    ScreeningCompleted:
      summary: Completed screening of the example FWB
      value:
        id: scr_01JC4T8Z6Q2W9V3M7K5N0P1R2S
        object: screening
        reference: CAI-BKG-2026-118842
        environment: sandbox
        status: completed
        outcome: review
        created_at: '2026-11-03T09:14:02Z'
        completed_at: '2026-11-03T09:14:51Z'
        metadata: {}
        shipment:
          source: fwb
          awb: 235-12345675
          houses: []
          origin: { airport: AMS, country: NL }
          destination: { airport: IST, country: TR }
          export_country: NL
          flights: [{ flight: TK1958, date: '2026-11-14' }]
          parties:
            - id: pty_1
              house_id: null
              role: shipper
              name: TULIPA PRECISION METALS BV
              type: organisation
              address: { street: INDUSTRIEWEG 12, city: EINDHOVEN, postal_code: 5651GK, country: NL }
            - id: pty_2
              house_id: null
              role: consignee
              name: BOSPORUS TEKNIK MAKINA AS
              type: organisation
              address: { street: ORGANIZE SANAYI BOLGESI 4 CAD NO 7, city: ISTANBUL, postal_code: '34906', country: TR }
            - id: pty_3
              house_id: null
              role: agent
              name: NORTHDYKE FORWARDING BV
              type: organisation
              address: { city: AMSTERDAM, country: NL }
          goods:
            - { id: gds_1, house_id: null, description: MARAGING STEEL BARS, hs_code: '72281050', pieces: 2, gross_weight_kg: 480.0 }
            - { id: gds_2, house_id: null, description: STEEL RETAINING RINGS, hs_code: '73182900', pieces: 1, gross_weight_kg: 132.0 }
        warnings:
          - code: party_address_incomplete
            message: Agent has no street address; screened on name, city and country.
            path: parties[2].address
        checks:
          denied_party:
            status: completed
            outcome: clear
            completed_at: '2026-11-03T09:14:04Z'
            error: null
            results:
              - { party_id: pty_1, outcome: clear, matches: [] }
              - { party_id: pty_2, outcome: clear, matches: [] }
              - { party_id: pty_3, outcome: clear, matches: [] }
          trade_regulation:
            status: completed
            outcome: review
            completed_at: '2026-11-03T09:14:05Z'
            error: null
            jurisdiction: EU
            sanctions_target: TR
            results:
              - goods_id: gds_1
                hs_code: '72281050'
                outcome: review
                regimes:
                  - { regime: dual_use, result: match }
                  - { regime: military, result: no_match }
                  - { regime: sanctions, result: not_applicable }
                controlled_items:
                  - regime: dual_use
                    control_number: 1C216
                    hs_code: '72281050'
                    category: 1 - Special materials and related equipment / C - Materials
                    description: "Maraging steel, other than that specified in 1C116, 'capable of' an ultimate tensile strength of 1 950 MPa or more, at 293 K (20 °C). Note: 1C216 does not control forms in which all linear dimensions are 75 mm or less. Technical note: the phrase maraging steel 'capable of' encompasses maraging steel before or after heat treatment."
                    control_list:
                      name: Regulation (EU) 2021/821 — Union regime for the control of exports, brokering, technical assistance, transit and transfer of dual-use items
                      reference: 02021R0821-20251115
                      publisher: European Union
                      url: https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:02021R0821-20251115
                    note: The match is a correlation via the HS code. The goods fall under 1C216 only if they meet its technical specification; an export licence is then required.
              - goods_id: gds_2
                hs_code: '73182900'
                outcome: clear
                regimes:
                  - { regime: dual_use, result: no_match }
                  - { regime: military, result: no_match }
                  - { regime: sanctions, result: not_applicable }
                controlled_items: []
          kyc_ai:
            status: completed
            outcome: review
            completed_at: '2026-11-03T09:14:51Z'
            error: null
            results:
              - party_id: pty_1
                outcome: clear
                confidence: 96
                summary: Registered Dutch private company at the declared address, active, manufacturer of precision metal parts.
                findings:
                  - { field: name, declared: TULIPA PRECISION METALS BV, found: Tulipa Precision Metals B.V., verdict: confirmed, confidence: 98, explanation: Company register lists the same legal name; capitalisation and punctuation differ only., sources: ['https://register.example/nl/tulipa-precision-metals'] }
                  - { field: street, declared: INDUSTRIEWEG 12, found: Industrieweg 12, verdict: confirmed, confidence: 95, explanation: Registered office address matches., sources: ['https://register.example/nl/tulipa-precision-metals'] }
                  - { field: activity_status, declared: null, found: active, verdict: confirmed, confidence: 94, explanation: Registration is active; no dissolution or bankruptcy notice found., sources: ['https://register.example/nl/tulipa-precision-metals'] }
              - party_id: pty_2
                outcome: review
                confidence: 71
                summary: Company found in Istanbul with a matching name, but the registered postal code differs from the declared one.
                findings:
                  - { field: name, declared: BOSPORUS TEKNIK MAKINA AS, found: Bosporus Teknik Makina A.Ş., verdict: confirmed, confidence: 93, explanation: Trade registry gazette lists the company under this name., sources: ['https://gazette.example/tr/bosporus-teknik'] }
                  - { field: postal_code, declared: '34906', found: '34953', verdict: differs, confidence: 80, explanation: The trade registry and the company website both give 34953 (Tuzla organised industrial zone)., sources: ['https://gazette.example/tr/bosporus-teknik', 'https://bosporus-teknik.example/contact'] }
        usage:
          billable_checks: 3
          items:
            - { check: denied_party, units: 3 }
            - { check: trade_regulation, units: 4 }
            - { check: kyc_ai, units: 2 }

Still provisional