Nexiel Screen API

Every endpoint, parameter, and response schema for Nexiel Screen. Fetch the same spec as a machine-readable document at GET /openapi.json.

OpenAPI 3.1.0Version 0.1.0https://compliance.nexiel.io

Requests to a production deployment require a bearer token. Mint one via the standard OAuth2 client-credentials grant against https://auth.nexiel.io/realms/nexiel/protocol/openid-connect/token using the client_id/client_secretpair from your dashboard's API Keys page — see this API's own quickstart for the exact curl command.

GET/health

Liveness / service-mode check

Reports the service-level mode (sandbox by default). This is distinct from a screening request's own mode (see POST /v1/screen). Returns 503 with status "draining" during the brief shutdown window between receiving SIGTERM and the process actually exiting, so a load balancer can stop routing new requests to this instance before it stops listening.

Responses

200
503
POST/v1/screenBearer auth (production mode)

Screen a name and/or cryptocurrency wallet addresses against sanctions/PEP data

Runs the 4-layer hybrid matching engine (exact, full-text, Levenshtein, and phonetic, plus an AI/vector layer that is shadow-mode only and not yet used for live decisions) against `name`, `dob`, and `nationality`, and independently, if `walletAddresses` is supplied, an exact-match wallet-address lookup against sanctioned cryptocurrency addresses (currently OFAC SDN only, exact-list matching only, no exposure or clustering analysis, no entity attribution beyond what the source list states). The two checks are genuinely independent: a request may supply either, both, or neither. The returned top-level `classification` is the more severe of the two, and `wallet` always reports exactly which addresses were checked and which matched, even when none were supplied. In sandbox mode (the default, see the top-level API description) neither check ever touches real ETL-sourced data. `meta.humanReviewRequired` is the EU AI Act human-in-the-loop flag for POSSIBLE_MATCH results (wallet screening never produces POSSIBLE_MATCH: an address match is always CONFIRMED_MATCH, never an ambiguous score). `Idempotency-Key` is enforced server-side (see the header description): a repeated key with the exact same request returns the original response without re-screening, and the same key with a different request (including a different set of walletAddresses) returns a 409. Authenticated (production-mode) requests are also per-client rate limited with a token bucket: exceeding it returns a `429` with a `Retry-After` header. Separately, a per-client, self-baselined anomaly signal is evaluated on every authenticated request, covering three dimensions: request volume (this hour's count against that client's own trailing-24h baseline), geography (a country never seen before for that client, once at least one other country is already on record), and behavior shift (this hour's mix of check kinds against that client's own trailing-24h mix, measured as total variation distance). Volume is the one signal with a real, automatic enforcement action today: a genuine spike temporarily reduces that client's own rate limit for a bounded cooldown window, recorded as a `CLIENT_AUTO_THROTTLED` audit event. It self-expires and is never a permanent block. Geography and behavior shift are logged for operators only and never throttle or block a request, a deliberate choice in line with the EU AI Act's human-in-the-loop requirement, not a temporary gap. If your client has configured a source restriction (an IP allowlist and/or an allowed request domain), a request from outside it returns `403 client_access_restricted` regardless of an otherwise-valid credential — off by default; contact us to enable it.

Parameters

idempotency-key (header)string

Client-supplied idempotency key. Enforced server-side for 24h: resending the exact same request with the same key returns the ORIGINAL response verbatim, without re-running the screen or re-emitting an audit_log/shadow_scores entry. Reusing the same key with a DIFFERENT request body is a 409 (`idempotency_key_conflict`) rather than silently overwriting or ignoring the mismatch, so use a new key per distinct request. Omitting the header is safe. No dedup is applied, and every request is screened independently, as before.

Request body

namestringrequired
dobstring
nationalitystring
walletAddressesstring[]

Cryptocurrency wallet addresses to screen against sanctioned-address lists (currently OFAC SDN only), exact-match only, in any raw form (mixed-case EIP-55 checksummed hex, bech32, legacy Base58Check, and so on), normalized server-side before comparison. Independent of name, dob, and nationality: a request may supply either, both, or neither.

mode"sandbox" | "live"

A name with no relation to any seeded entity

{
  "name": "Zzqxw Unrelated Nobody"
}

Exact name + DOB + nationality match against a sandbox entity

{
  "name": "Anatoly Sandboxovich Testperson",
  "dob": "1960-01-01",
  "nationality": "RU"
}

Close name match with a contradicting DOB (fuzzy layers surface it, human review required)

{
  "name": "Anatoly Sandboxovich Testperson",
  "dob": "1999-09-09"
}

Responses

200

CLEAR: no candidates above threshold

{
  "classification": "CLEAR",
  "candidates": [],
  "wallet": {
    "classification": "CLEAR",
    "matches": [],
    "checkedAddresses": []
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false,
    "humanReviewRequired": false,
    "decisionId": "41",
    "engineVersion": "0.1.0-provisional",
    "gatesHash": "212b018655f47f1514c4b404a02e643ef027b5039964d5c68dcdd5ec0dff8f3b",
    "ensembleWeightsHash": "4a2723f74864b3371c860bbb93c3d6fbfde7dcc21e99e1242ddad3ff366cf69d",
    "listSnapshotIds": []
  }
}

CONFIRMED_MATCH: exact-match short circuit

{
  "classification": "CONFIRMED_MATCH",
  "candidates": [
    {
      "entityId": "a33e0af1-d984-451b-b0b5-7c6c3759b977",
      "source": "sandbox",
      "listRef": "SANDBOX-1",
      "entityType": "INDIVIDUAL",
      "isPep": false,
      "pepCategory": null,
      "pepFunctionalCategory": null,
      "pepStatus": null,
      "pepJurisdiction": null,
      "linkedPeps": [],
      "matchedVia": [
        "exact",
        "fts",
        "phonetic",
        "trigram"
      ],
      "classifyResult": {
        "score": 100,
        "classification": "CONFIRMED_MATCH",
        "floored": false,
        "exactMatchShortCircuit": true,
        "explain": {
          "levenshtein": 1,
          "phonetic": 1,
          "dobNat": {
            "dob": 1,
            "nationality": 1,
            "combined": 1
          },
          "vector": null,
          "weightSet": "launch"
        },
        "engineVersion": "0.1.0-provisional"
      }
    }
  ],
  "wallet": {
    "classification": "CLEAR",
    "matches": [],
    "checkedAddresses": []
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false,
    "humanReviewRequired": false,
    "decisionId": "42",
    "engineVersion": "0.1.0-provisional",
    "gatesHash": "212b018655f47f1514c4b404a02e643ef027b5039964d5c68dcdd5ec0dff8f3b",
    "ensembleWeightsHash": "4a2723f74864b3371c860bbb93c3d6fbfde7dcc21e99e1242ddad3ff366cf69d",
    "listSnapshotIds": []
  }
}

POSSIBLE_MATCH: humanReviewRequired is true (EU AI Act human-in-the-loop gate)

{
  "classification": "POSSIBLE_MATCH",
  "candidates": [
    {
      "entityId": "a33e0af1-d984-451b-b0b5-7c6c3759b977",
      "source": "sandbox",
      "listRef": "SANDBOX-1",
      "entityType": "INDIVIDUAL",
      "isPep": false,
      "pepCategory": null,
      "pepFunctionalCategory": null,
      "pepStatus": null,
      "pepJurisdiction": null,
      "linkedPeps": [],
      "matchedVia": [
        "exact",
        "fts",
        "phonetic",
        "trigram"
      ],
      "classifyResult": {
        "score": 87,
        "classification": "POSSIBLE_MATCH",
        "floored": false,
        "exactMatchShortCircuit": false,
        "explain": {
          "levenshtein": 1,
          "phonetic": 1,
          "dobNat": {
            "dob": 0.1,
            "nationality": 0.5,
            "combined": 0.3
          },
          "vector": null,
          "weightSet": "launch"
        },
        "engineVersion": "0.1.0-provisional"
      }
    }
  ],
  "wallet": {
    "classification": "CLEAR",
    "matches": [],
    "checkedAddresses": []
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false,
    "humanReviewRequired": true,
    "decisionId": "43",
    "engineVersion": "0.1.0-provisional",
    "gatesHash": "212b018655f47f1514c4b404a02e643ef027b5039964d5c68dcdd5ec0dff8f3b",
    "ensembleWeightsHash": "4a2723f74864b3371c860bbb93c3d6fbfde7dcc21e99e1242ddad3ff366cf69d",
    "listSnapshotIds": []
  }
}
400

Required `name` field missing (schema validation failure)

{
  "error": "bad_request",
  "message": "body must have required property 'name'"
}
401

No Authorization header sent while the service runs in production mode

{
  "error": "unauthorized",
  "message": "missing or malformed Authorization header (expected \"Bearer <token>\")"
}

Token is expired, or its Keycloak client was revoked (introspection active=false)

{
  "error": "unauthorized",
  "message": "token is expired, revoked, or otherwise not active"
}
403
409

The same Idempotency-Key was already used for a different request

{
  "error": "idempotency_key_conflict",
  "message": "Idempotency-Key was already used with a different request body/params. Use a new, unique key for a genuinely different request, or resend the exact original request to get back its cached result."
}
429

Client's per-client rate limit (token bucket) was exhausted

{
  "error": "rate_limit_exceeded",
  "message": "rate limit exceeded for this client; retry after 2s. This is a per-client request-rate limit (CLAUDE.md Security Requirements), not an error in your request — resend after the given delay.",
  "retryAfterSeconds": 2
}
500
GET/v1/screen/{decisionId}Bearer auth (production mode)

Fetch one of your own past screening decisions, including your own review status

Read-only. `decisionId` is `meta.decisionId` from a prior POST /v1/screen response. Scoped strictly to the calling API key's own client_id. A decisionId that is real but belongs to a different client 404s exactly like one that does not exist at all. `meta.humanReviewRequired` reflects whether your own review is still outstanding (true only while classification is POSSIBLE_MATCH and `customerReviews` is empty), never the stale value frozen at decision time.

Parameters

decisionId (path)stringrequired

audit_log.client_seq of a SCREENING_DECISION belonging to the calling client. See meta.decisionId on POST /v1/screen.

Responses

200
401
404
500
POST/v1/screen/{decisionId}/reviewBearer auth (production mode)

Record your own review of one of your own POSSIBLE_MATCH screening decisions

Under the EU AI Act, a POSSIBLE_MATCH result always requires human review before any action is taken on it, and is never auto-cleared. That human is you, the regulated client. Nexiel never reviews or resolves your matches on your behalf (see /legal: you retain full and final responsibility for your own compliance decisions). `decisionId` is `meta.decisionId` from a prior POST /v1/screen response. Only ever accepted for a decision that (a) belongs to the calling client (404 otherwise, identical to a decisionId that does not exist at all, never a 403 that would confirm another client's data exists) and (b) is currently classified POSSIBLE_MATCH (409 `screening_decision_not_reviewable` otherwise, since a CLEAR or CONFIRMED_MATCH decision was never awaiting review in the first place). Never mutates the original decision's `classification`, which stays exactly as originally computed, part of the frozen decision artifact. Your review is recorded as its own, separate, immutable audit event. Re-review is allowed (submitting again records a new review, nothing is overwritten). Every one of your past reviews for this decision is returned in `customerReviews`, most recent first. `Idempotency-Key` is enforced server-side exactly like POST /v1/screen's own header.

Parameters

decisionId (path)stringrequired

audit_log.client_seq of a SCREENING_DECISION belonging to the calling client. See meta.decisionId on POST /v1/screen.

idempotency-key (header)string

Client-supplied idempotency key. Enforced server-side for 24h: resending the exact same request with the same key returns the ORIGINAL response verbatim, without recording a second review event. Reusing the same key with a DIFFERENT request body/decisionId is a 409 (`idempotency_key_conflict`).

Request body

decision"confirmed_match" | "false_positive" | "escalated"required
notesstring

Responses

200
400
401
404
409
500
POST/v1/wallet-risk/scoreBearer auth (production mode)

[PREVIEW] Score a cryptocurrency wallet address using GraphSense TagPacks attribution data

Preview, sandbox-only surface. Not yet a billed, generally available product, and there is no pricing tier for this route yet. Looks up the given (network, address) against the real, ingested GraphSense TagPacks corpus (live mode) or a small, clearly fake synthetic fixture set (sandbox mode, the default, see the top-level API description) and returns a 0-100 risk score plus the exact evidence tags behind it. Honest scope: this detects only an address directly present in the ingested corpus. There is no blockchain-transaction-hop exposure propagation (no transaction-graph data is ingested at all), so an address that received funds from a tagged address is invisible to this engine unless it also carries its own direct tag. `sameActorAddresses` is not exposure propagation either. It only surfaces other addresses the corpus already tags under the same declared actor. A null `score` and `classification: "UNTAGGED"` means "no evidence found in this corpus," an absence of evidence, never proof the address is safe. This is explicitly not Chainalysis, TRM, or Elliptic-equivalent coverage. `frozenDecisionArtifact` pins exactly which engine version, risk-weights config, and TagPacks ingestion snapshot produced this score. `tagpackEtlRunId` is always null in sandbox mode (synthetic data has no ETL lineage to cite). `Idempotency-Key` is enforced server-side exactly like POST /v1/screen's own header. Authenticated (production-mode) requests share that same route's per-client rate limit and anomaly tracking.

Parameters

idempotency-key (header)string

Client-supplied idempotency key. Enforced server-side for 24h: resending the exact same request with the same key returns the ORIGINAL response verbatim, without re-running the score or re-emitting an audit_log entry. Reusing the same key with a DIFFERENT request body is a 409 (`idempotency_key_conflict`).

Request body

networkstringrequired

Chain/network code (e.g. "BTC", "ETH"). Free text, normalized (trimmed and upper-cased) server-side before lookup.

addressstringrequired

The wallet address to score, in any raw form. Normalized server-side in a network-aware way: case-insensitive hex networks like ETH/BSC are lowercased, and Base58Check chains are compared byte-exact and case-sensitive, since their checksum depends on case.

mode"sandbox" | "live"

Sandbox synthetic LOW-risk fixture (a fake "exchange"-tagged address)

{
  "network": "BTC",
  "address": "1SandboxLowRiskExchangeFAKE00001"
}

Sandbox synthetic HIGH-risk fixture (a fake terrorism-financing-tagged address)

{
  "network": "BTC",
  "address": "1SandboxHighRiskTerrorismFAKE003"
}

Responses

200

LOW: a single, low-severity synthetic tag (sandbox mode)

{
  "network": "BTC",
  "address": "1SandboxLowRiskExchangeFAKE00001",
  "addressLookup": "1SandboxLowRiskExchangeFAKE00001",
  "score": 6,
  "classification": "LOW",
  "matchedTags": [
    {
      "network": "BTC",
      "address": "1SandboxLowRiskExchangeFAKE00001",
      "label": "Sandbox Synthetic Exchange (fake — for API testing only, not a real entity)",
      "category": "exchange",
      "abuse": null,
      "concepts": [],
      "actorId": null,
      "confidence": "service_data",
      "confidenceLevel": 50,
      "categoryWeight": 12,
      "contributionScore": 6,
      "isSanctionsConcept": false,
      "source": null,
      "packFile": "sandbox/synthetic-fixtures",
      "packTitle": "Nexiel Sandbox Synthetic Fixtures (fake data — never from GraphSense TagPacks)",
      "lastmod": null,
      "isClusterDefiner": false
    }
  ],
  "excludedTags": [],
  "sameActorAddresses": [],
  "frozenDecisionArtifact": {
    "engineVersion": "0.1.0",
    "riskWeightsHash": "be3b2339f08e725fa510d37c5fdc0b4364a1143601dcd209cf355b946146f298",
    "tagpackEtlRunId": null
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false
  }
}

HIGH: a maximum-severity synthetic tag (sandbox mode)

{
  "network": "BTC",
  "address": "1SandboxHighRiskTerrorismFAKE003",
  "addressLookup": "1SandboxHighRiskTerrorismFAKE003",
  "score": 100,
  "classification": "HIGH",
  "matchedTags": [
    {
      "network": "BTC",
      "address": "1SandboxHighRiskTerrorismFAKE003",
      "label": "Sandbox Synthetic Terrorism-Financing Wallet (fake — for API testing only, not a real entity)",
      "category": null,
      "abuse": "terrorism_financing",
      "concepts": [],
      "actorId": null,
      "confidence": "ledger_immanent",
      "confidenceLevel": 100,
      "categoryWeight": 100,
      "contributionScore": 100,
      "isSanctionsConcept": false,
      "source": null,
      "packFile": "sandbox/synthetic-fixtures",
      "packTitle": "Nexiel Sandbox Synthetic Fixtures (fake data — never from GraphSense TagPacks)",
      "lastmod": null,
      "isClusterDefiner": false
    }
  ],
  "excludedTags": [],
  "sameActorAddresses": [],
  "frozenDecisionArtifact": {
    "engineVersion": "0.1.0",
    "riskWeightsHash": "be3b2339f08e725fa510d37c5fdc0b4364a1143601dcd209cf355b946146f298",
    "tagpackEtlRunId": null
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false
  }
}

UNTAGGED: no evidence found (absence of evidence, never proof of safety)

{
  "network": "BTC",
  "address": "1SandboxNoDataAtAllFAKE0000000007",
  "addressLookup": "1SandboxNoDataAtAllFAKE0000000007",
  "score": null,
  "classification": "UNTAGGED",
  "matchedTags": [],
  "excludedTags": [],
  "sameActorAddresses": [],
  "frozenDecisionArtifact": {
    "engineVersion": "0.1.0",
    "riskWeightsHash": "be3b2339f08e725fa510d37c5fdc0b4364a1143601dcd209cf355b946146f298",
    "tagpackEtlRunId": null
  },
  "meta": {
    "mode": "sandbox",
    "sandboxOverride": false
  }
}
400
401
409
429
500
POST/v1/onboarding/kyb-applications

[Onboarding Agent] Draft a KYB onboarding recommendation from signup input (never a decision)

Normalizes free-text signup input (via a pluggable InputDrafter: a real Anthropic-backed implementation in production, a deterministic rule-based test double in sandbox mode and tests), runs the same real registry verification, UBO/director plausibility check, and entity plus person sanctions/PEP screening a real KYB application would, and drafts exactly one `kyb_onboarding_drafts` row. It never creates a real client or KYB application. `recommendation` is advisory only (SUBMIT_RECOMMENDED, NEEDS_INFO_RECOMMENDED, or DO_NOT_SUBMIT_RECOMMENDED), deliberately spelled nothing like a real kyb_applications.status value. A human reviewer must explicitly act on this draft before any real KYB application exists for it, and that application always starts NEEDS_REVIEW regardless of how clean this draft looked. Only a second, independent human decision can ever approve or reject it. `advisoryFlags` are purely informational normalization notes, never a gate. `Idempotency-Key` is enforced server-side, scoped to this applicant (registryCountry and registryNumber) since no client identity exists yet at this point in the flow. This endpoint has no client identity to authenticate or rate-limit by yet, so abuse protection here is a pre-auth, source-IP-keyed token bucket with a deliberately low burst and hourly budget, rather than the per-client limiter every authenticated route in this API uses. Exceeding it returns a `429` with a `Retry-After` header, the same contract as `POST /v1/screen`'s own rate limit.

Parameters

idempotency-key (header)string

Client-supplied idempotency key, scoped to this applicant (registryCountry and registryNumber). Resending the exact same submission with the same key returns the ORIGINAL result verbatim, without submitting a second KYB application. Reusing the same key with a materially different submission is a 409. Omitting the header is safe (no dedup applied).

Request body

legalNamestringrequired
registryCountrystringrequired
registryNumberstringrequired
personsobject[]required
rolestringrequired
fullNamestringrequired
dobstring
nationalitystring
ownershipPercentagenumber | string
controlBasisstring
contactEmailstring

Responses

200
400
409
429
500
POST/v1/kyb-applications/{applicationId}/review-draftsBearer auth (production mode)

[KYB Review Copilot] Generate a draft recommendation for a NEEDS_REVIEW application

Reads the application's already-produced evidence (registry check, UBO plausibility, entity and person screening, all from the application's real workflow, never re-derived here) and persists a new draft recommendation (`APPROVE_RECOMMENDED`, `REJECT_RECOMMENDED`, or `ESCALATE_RECOMMENDED`) with a plain-English rationale. This is advisory only: it never changes the application's real review status, and the human reviewer's own decision is the only thing that can. 404 if the application does not exist. 409 if it is not currently NEEDS_REVIEW (for example, already decided).

Parameters

applicationId (path)stringrequired

Responses

201
401
404
409
500
GET/v1/kyb-applications/{applicationId}/review-drafts/latestBearer auth (production mode)

[KYB Review Copilot] Fetch the latest persisted draft recommendation

Read-only: returns the most recently generated draft for this application without generating a new one. 404 if none has been generated yet.

Parameters

applicationId (path)stringrequired

Responses

200
401
404
500
GET/v1/screen/gdpr/exportBearer auth (production mode)

GDPR Article 15 (right of access): export everything Nexiel Screen holds about the calling API key

Scoped strictly to the authenticated bearer token's own client_id, never another client's data. Includes this key's own audit_log entries and, if this key is linked (via client_api_keys) to a KYB/CDD record, that client's clients, kyb_applications, kyb_ubos, and kyb_checks rows in full. This is self-declared UBO/director data the client itself submitted: Article 15 access is never legally constrained, only Article 17 erasure is. `kyb.linked: false` means no such record is on file for this specific API key (for example, a key minted before this linkage existed), reported explicitly, never silently omitted.

Responses

200
401
500
POST/v1/screen/gdpr/erasure-requestsBearer auth (production mode)

GDPR Article 17 (right to erasure), scoped to what is legally safe to erase today

Deletes only idempotency_keys rows for this caller (already PII-free cached responses). Never touches kyb_ubos, kyb_applications, or kyb_checks: EU AML Directives require retaining self-declared UBO/director CDD records for the life of the business relationship plus a statutory period. Erasing them requires a documented legal determination that the period has lapsed, made by the founder or engaged counsel, never by this endpoint. Also never touches audit_log, an append-only hash chain. `requiresLegalReview` is non-null exactly when this caller has a linked KYB/CDD record, and reports counts only, never the underlying data.

Responses

200
401
500
GET/v1/screen/usageBearer auth (production mode)

Per-client API usage: request volume by day/month, and screening decisions by classification

Scoped strictly to the authenticated bearer token's own client_id, never another client's usage. `requestVolume` comes from this service's existing per-client, per-service, hourly request-volume tracking (the same tracking anomaly detection also uses). `decisions` comes from this service's existing audit_log SCREENING_DECISION events. Defaults to a trailing 90-day window ending now when `from` and `to` are omitted. The widest single request this endpoint accepts is 366 days.

Parameters

from (query)string

Start of the reporting window (inclusive), UTC calendar date. Defaults to 90 days before `to`.

to (query)string

End of the reporting window (exclusive), UTC calendar date. Defaults to today.

Responses

200
400
401
500
POST/v1/sealBearer auth (production mode)

Seal a PDF document with a Qualified Electronic Seal (QSeal)

Seals a PDF document on behalf of a KYB-approved client company, using that company's own CSC (Cloud Signature Consortium) API v2.2 credential at its QTSP (Qualified Trust Service Provider), never a shared Nexiel credential. A QSeal cryptographically attributes the seal to whoever's certificate signs it, so using any credential other than the requesting client's own would misattribute the seal. Gated by, in order: real Keycloak authentication, the existing account-active check (which includes KYB-approved status), this client having a provisioned seal credential (`clients.qseal_credential_id`), and, as the QSeal-specific check, `authorizingRepresentativeId` resolving to a real declared UBO/director/senior-managing-official on file for this client's own APPROVED KYB application. Any one such declared person may authorize a seal alone (a single-signer model). `frozenDecisionArtifact` pins exactly which CSC credential, QTSP, engine version, and CSC API spec version produced this seal. Nexiel never stores the document itself, only SHA-256 hashes of the original and sealed bytes. No real QTSP account exists yet, so this route currently always returns 503 `qseal_not_configured`. This is real, tested scaffolding built against the published CSC API v2.2 specification, ready to point at a real QTSP sandbox once credentials exist. `Idempotency-Key` is enforced server-side exactly like every other mutating endpoint in this service.

Parameters

idempotency-key (header)string

Client-supplied idempotency key. Enforced server-side for 24h: resending the exact same request with the same key returns the ORIGINAL response verbatim, without re-sealing the document or re-emitting a qseal_operations/audit_log entry. Reusing the same key with a DIFFERENT request body is a 409 (`idempotency_key_conflict`).

Request body

documentstringrequired

Base64-encoded PDF document to seal.

authorizingRepresentativeIdstringrequired

The kyb_ubos.id of the declared UBO/director/senior-managing-official (on file for this client's own APPROVED KYB application) authorizing this seal. Must be a real row already on file. This endpoint never accepts a free-text name or other caller-asserted identity.

Responses

200
400
401
403
409
429
500
503
POST/v1/signBearer auth (production mode)

Sign a PDF document with a Qualified Electronic Signature (QES)

Signs a PDF document on behalf of a declared representative of a KYB-approved client company, using that representative's own, individually-provisioned CSC (Cloud Signature Consortium) API v2.2 credential at its QTSP (Qualified Trust Service Provider), never a shared Nexiel credential. Contrast POST /v1/seal, which attributes a document to the LEGAL PERSON (the company) via one shared credential -- a QES attributes it to the NATURAL PERSON signing, via that person's own credential, per eIDAS Art. 3(12). Gated by, in order: real Keycloak authentication, the existing account-active check (which includes KYB-approved status), `signerId` resolving to a real declared UBO/director/senior-managing-official on file for this client's own APPROVED KYB application, and that signer having their own individually-provisioned QES credential (`kyb_ubos.qes_credential_id`). An optional `pin` / `pinObjectId` pair is forwarded to the QTSP as the CSC API `authData` object to authorize this specific signature (eIDAS Art. 26 "sole control") and is never logged or persisted anywhere in this codebase. `frozenDecisionArtifact` pins exactly which CSC credential, QTSP, engine version, and CSC API spec version produced this signature. Nexiel never stores the document itself, only SHA-256 hashes of the original and signed bytes. No real QTSP account exists yet, so this route currently always returns 503 `qes_not_configured`. This is real, tested scaffolding built against the published CSC API v2.2 specification, ready to point at a real QTSP sandbox once credentials exist. `Idempotency-Key` is enforced server-side exactly like every other mutating endpoint in this service.

Parameters

idempotency-key (header)string

Client-supplied idempotency key. Enforced server-side for 24h: resending the exact same request with the same key returns the ORIGINAL response verbatim, without re-signing the document or re-emitting a qes_operations/audit_log entry. Reusing the same key with a DIFFERENT request body is a 409 (`idempotency_key_conflict`).

Request body

documentstringrequired

Base64-encoded PDF document to sign.

signerIdstringrequired

The kyb_ubos.id of the declared UBO/director/senior-managing-official (on file for this client's own APPROVED KYB application, with their own provisioned QES credential) signing this document AS THEMSELVES. Must be a real row already on file. This endpoint never accepts a free-text name.

pinstring

The signer's own PIN (or other CSC "explicit" auth-mode value), forwarded to the QTSP to authorize this specific signature and never logged or persisted by Nexiel. Omit for a non-interactive-mode credential.

pinObjectIdstring

The CSC authentication-object id `pin` answers (e.g. "PIN"), per the credential's own credentials/info authInfo.objects[].id. Required together with `pin`.

Responses

200
400
401
403
409
429
500
503