---
title: "Analyze a legal document"
method: POST
path: "/api/v1/analyze"
tags: ["Analysis"]
---

# Analyze a legal document

`POST /api/v1/analyze`

## Request

- Base URL: `https://legal.nexusquantum.legal`
- URL: `https://legal.nexusquantum.legal/api/v1/analyze`
- Auth: HTTP bearer

Send the text of a legal document and receive a structured analysis generated by the Nexus Legal multi-agent engine. The credit is debited only after a successful analysis.

Supports the optional `Idempotency-Key` header (8-200 alphanumeric chars + `-_`). If sent, repeating the same request with the same key returns the cached response WITHOUT charging additional credits (24h TTL). If the body changes, it returns 409 Conflict.

**⚠️ This endpoint is synchronous, and the gateway closes the connection at 300 s.** That ceiling is not ours and cannot be raised from your side. MEASURED on 2026-08-18: a BYO account whose engine is a *reasoning* model (DeepSeek V-pro, o1-style, Claude 5 with thinking) running `mode: "auditoria"` over a ~5 KB contract went past it and returned `502` — the engine spends a large part of its budget on internal reasoning before it starts writing, and a second pass with a wider budget is issued automatically when it runs out. That combination belongs on the asynchronous path: `POST /api/v1/jobs` with `kind: "analyze"` returns `202` and a `jobId` to poll, with no wall clock. The synchronous endpoint remains the right one for platform engines and for `mode: "standard"`.

## Headers

- `Idempotency-Key` string

## Request body

- AnalyzeRequest
  - `text` string, required: Legal document text to analyze.
  - `jurisdiction` string, required: ISO country code. REQUIRED — no jurisdiction is assumed by default (400 MISSING_JURISDICTION if absent). A declared but unserved jurisdiction returns 422 JURISDICTION_NOT_SERVED, and the message lists the served ones. See GET /api/v1/jurisdictions for the full declared list.
  - `legalBranch` 'adgm' | 'administratif' | 'administrativo' | 'agrario' | 'all' | 'ambiental' | 'amministrativo' | 'amparo' | 'appi' | 'arbeids' | 'arbeitsrecht' | 'arbitraje' | 'arbitration' | 'ayudas' | 'bankruptcy' | 'basic_law' | 'bbbee' | 'belasting' | 'bestuurs' | 'betreibungsrecht' | 'burgerlijk' | 'chiteki' | 'civil' | 'civile' | 'comercial' | 'commercial' | 'commerciale' | 'company' | 'competencia' | 'competition' | 'concursal' | 'constitucional' | 'constitutional' | 'constitutionnel' | 'consumidor' | 'consumo' | 'contract' | 'corporate' | 'costituzionale' | 'criminal' | 'datos' | 'difc' | 'dpdp' | 'employment' | 'empresarial' | 'equity' | 'eu' | 'eu_recht' | 'eu_retained' | 'europeen' | 'europeo' | 'europeu' | 'family' | 'fema' | 'financial' | 'finanzrecht' | 'fiscal' | 'foral' | 'fundamentales' | 'geunlobeop' | 'gyosei' | 'haengjeong' | 'handelsrecht' | 'heonbeop' | 'hyeongbeop' | 'immigration' | 'immobiliare' | 'immobilienrecht' | 'imobiliario' | 'inmobiliario' | 'insolvency' | 'insolvenzrecht' | 'intellectual' | 'intellectueel' | 'internacional' | 'inversiones' | 'investment' | 'jijeokjaesan' | 'keiji' | 'laboral' | 'labour' | 'lavoro' | 'ma' | 'mercado' | 'mercantil' | 'minbeop' | 'minero' | 'minpo' | 'minsa' | 'minso' | 'native_title' | 'obligationenrecht' | 'ondernemings' | 'pdpa' | 'pdpo' | 'penal' | 'penale' | 'pipa' | 'popia' | 'privacy' | 'procesal' | 'processual' | 'processuale' | 'processuel' | 'property' | 'propiedad' | 'propriete' | 'prozessrecht' | 'quebec' | 'rodo' | 'sangbeop' | 'sebeop' | 'sharia' | 'shoho' | 'steuerrecht' | 'straf' | 'strafrecht' | 'tax' | 'tort' | 'trabalho' | 'travail' | 'tributario' | 'verfassungsrecht' | 'verwaltungsrecht' | 'zei' | 'zivilrecht': Legal branch of the analysis. The enum is the union of the branches of ALL supported jurisdictions — `contract`, `corporate`, `employment`, `property`, `pdpa`, `arbitration`, `financial` and the rest of the common-law ones alongside the continental-law codes — and the server validates against that same union: the branch is NOT restricted per jurisdiction. Send the branch your jurisdiction actually uses. ⚠️ Omitting the field falls back to `civil`, a continental civil-law branch that carries no meaning in a common-law jurisdiction. That default is documented because the server really applies it, not because it is a sensible one — always send `legalBranch` explicitly.
  - `language` 'es' | 'en': Response language. Optional — when omitted, the language FOLLOWS the jurisdiction (per-jurisdiction registry: SG→en, ES/CO→es, CH→de, …). An explicit supported value takes precedence.
  - `mode` 'standard' | 'deep': Analysis mode. • `standard` (1 credit): Node A only (Claude Opus 4.7). • `deep` (2 credits): Node A + Node B (DeepSeek V4-Pro) audits it adversarially, looking for hallucinations, false citations, omissions and jurisdictional errors. The response includes an `audit` object with structured findings. Accepted compat aliases: `agil` ⇄ `standard`, `auditoria` ⇄ `deep`.
  - `professionalRole` 'persona' | 'abogado' | 'fiscal' | 'notario' | 'registrador' | 'juez' | 'asesor': Professional role the document is analyzed from. Changes the report's perspective. The values are Spanish-language IDENTIFIERS — send them verbatim, they are not display text: Either spelling works: `lawyer`/`solicitor`/`barrister`, `prosecutor`, `notary`, `registrar`, `judge`, `in-house`, `individual` - or the original wire values, which keep working unchanged: `abogado` (lawyer), `fiscal` (prosecutor), `notario` (notary), `registrador` (registrar), `juez` (judge), `asesor` (adviser), `persona` (non-professional individual).
  - `roleVariant` string: Optional local seat when a jurisdiction splits one function into more than one office. England & Wales splits `abogado` into `solicitor` and `barrister`, which look at different things: the first at client risk, deadlines and SRA conduct duties; the second at the strength of the argument before the court and the overriding duty to it. Additive — it never replaces `professionalRole`, which stays the wire value. A variant that does not exist in the active jurisdiction is not an error: that function simply is not split there, and the most common seat is used.
  - `proceduralSide` 'demandante' | 'demandado', nullable: Client's procedural position (demandante = claimant, demandado = respondent). Omit or `null` = neutral, no position. Modulates the perspective for the `abogado` and `persona` roles.
  - `negotiationProfile` 'conservador' | 'equilibrado' | 'agresivo': Analysis profile. Spanish-language IDENTIFIERS, sent verbatim: `conservador` (conservative — maximizes risk alerts), `equilibrado` (balanced) or `agresivo` (aggressive — filters out non-blocking alerts).
  - `referenceDate` string: Date of the case FACTS (lex temporis, YYYY-MM-DD). Deterministic citation verification checks that the cited rules were in force on this date, not today. Omit = today.

## Response `200`

Analysis completed successfully.

- AnalyzeResponse
  - `skill` 'standard' | 'agil' | 'auditoria', required: WHICH ANALYSIS CORE RAN. `mode` and `skill` are two different questions that share a name on the way IN: `mode` is the BILLING axis (does Node B audit? `standard` vs `deep`), `skill` is which Node A core was loaded. Sending `mode: "auditoria"` bills as `deep` AND loads the forensic-audit core, so the response comes back with `mode: "deep"` and `skill: "auditoria"` — and the report header says `MODE: auditoria`. Until 2026-08-18 only the billing axis came back, so that header looked like it contradicted the JSON. It never did.
  - `analysis` string, required: Legal analysis generated by Node A (Claude Opus 4.7).
  - `jurisdiction` string, required: ISO code of the processed jurisdiction.
  - `legalBranch` string, required: Legal branch applied.
  - `model` string, required: Label of the engine that ANSWERED, not a stable model identifier. It is the chain's own label — 'DeepSeek V4-Pro', 'Opus 4.8 · OpenRouter' — or, when the firm brings its own engine, that engine's model id. In mode=deep it is composed as '<analyst>+<auditor>'. `unknown` means nobody could be named: the value is never guessed. Under partner or neutral branding it is replaced by a neutral label. Do NOT key logic on this string: which engine answers is a deployment decision and the chain falls back on its own.
  - `processingMs` number, required: Total analysis latency in milliseconds.
  - `mode` 'standard' | 'deep', required: Canonical mode applied (aliases normalized).
  - `creditsCharged` integer: Credits charged for this analysis (1 standard / 2 deep).
  - `audit` AuditResult: Node B adversarial audit of the Node A analysis. Present only when mode=deep. IMPORTANT: check `audited` before reading `findings` or `overallSeverity`. When `audited` is false the audit produced no usable verdict, and those two fields are filler — an empty `findings` there means "we could not audit", NOT "nothing was found". Engine failures (no credit, rotated key, quota, unreachable provider) do NOT arrive here at all: they return 424 with a machine code, so a 200 with an audit always means your engine answered.
    - `auditor` string, required: Label of the engine that produced the audit. `unknown` when none could be named — which is what you get if every engine in the chain failed and `audited` is false. Same caveat as `model`: do not key logic on it.
    - `audited` boolean: True when the auditor produced a readable verdict. False when it answered but its output could not be parsed into one. Absent on responses predating 2026-07-26; treat absence as unknown rather than as true.
    - `text` string, required: Auditor's textual analysis, free-form.
    - `findings` AuditFinding[], required
      - `type` 'hallucination' | 'missing_clause' | 'weak_reasoning' | 'citation_error' | 'other', required: Audit finding category.
      - `severity` 'low' | 'medium' | 'high', required
      - `description` string, required: Specific description of the finding. Max 1000 chars.
    - `overallSeverity` 'low' | 'medium' | 'high', required
    - `recommendations` string, required: Specific recommendations, free-form.
    - `processingMs` number, required
  - `citationChecks` CitationVerificationSummary
    - `checks` CitationCheck[], required
      - `citation` string, required: Citation exactly as it appeared in the analysis.
      - `kind` 'L4-N' | 'L2-J', required: Legislation (L4-N) or case law (L2-J).
      - `status` 'verified' | 'mismatch' | 'not_found' | 'derogated' | 'pending' | 'not_verifiable', required: verified=exists and matches; mismatch=exists but diverges; not_found=absent from the active corpus; not_verifiable=jurisdiction with no active corpus; pending=not parseable.
      - `similarity` number, nullable, required: Lexical similarity (Jaccard) against the canonical text.
      - `semantic` number, nullable, required: Semantic similarity (cosine), only in the lexical gray zone.
      - `url` string, nullable, required: Official source URL for THAT jurisdiction when one is located (BOE · CENDOJ · Singapore Statutes Online · eLitigation · SUIN-Juriscol…).
      - `mismatchKind` 'meaning' | 'stitched' | 'lexical' | 'party': Only with `status: mismatch`, and ADDITIVE (may be missing from older checks). Grades the severity, which used to be lost: `meaning` = a meaning veto fired (negation, modality, quantifier, omitted exception, subject) → a REAL divergence of meaning. `stitched` = the quoted fragments are literal but do not come from a CONTINUOUS passage of the official text, or they alter its order — an ASSEMBLED quote. The defect is structural: a divergence of meaning has NOT been proven, so it must not be presented as one (added 2026-07-25; measured by contiguity since 2026-07-27). `lexical` = only the lexical ratio failed: an honest paraphrase that cannot be checked literally. `party` = the citation EXISTS but resolves to a DIFFERENT decision from the one the text names (incompatible party names — typically a transposed court, e.g. `Spandeck … [2007] SGHC 37` when the case is SGCA 37). It is neither a non-existent citation nor a divergence of meaning: it points elsewhere. `similarity` comes back `null` on purpose — the claim is not compared against a decision that is not the one named (added 2026-07-27).
      - `note` string, nullable: Human-readable note from the verifier (deterministic, server-side — never from the model). E.g. STATUTE-LEVEL verification of a law cited without an article, or the notice that the registry proved existence without returning the text.
      - `canonicalText` string, nullable: Official text the verifier downloaded to check the citation against, when the corpus returns it. For case law this is the full judgment: truncate it before displaying.
    - `counts` object, required: Count per status.
    - `anyBroken` boolean, required: There is at least one broken citation (mismatch/not_found/derogated). EXCLUDES `pending` (not parseable) and `not_verifiable` (jurisdiction with no active corpus): those statuses are NOT breakages. READ `composition` BEFORE ACTING ON THIS: `anyBroken: false` means every citation is faithful to its source ONE BY ONE. It does NOT mean the argument they support holds — the inferential step that joins them is not verified by this field.
    - `composition` object: SCOPE OF THE COMPOSITION — what `anyBroken` cannot tell you. Verifying each citation proves fidelity to its source; it says nothing about the step that JOINS them. Two impeccable citations can support a false conclusion. This object carries the axes of that step that ARE decidable without understanding the argument — repealed premises, premises not yet in force, and above all validity windows that never overlapped (a defect invisible when citations are checked one at a time) — and, just as important, the axes that are NOT checked, each with its reason. IT IS ALWAYS PRESENT, including when there are no findings: if it appeared only on problems, its absence would again mean "this is fine", which is the reading it exists to prevent. `no_comprobado` permanently includes `validez_logica`: WHETHER THE CONCLUSION FOLLOWS FROM THE PREMISES IS NOT CHECKED, and that is not a pending task — it is not a mechanically decidable question. An empty `hallazgos` is not a statement that the reasoning is valid.
      - `comprobado` string[]: Axes actually examined. An axis only enters here if there was MATERIAL for it to produce a finding — never merely because the code ran.
      - `hallazgos` object[]: ANOMALIES. Named, never an aggregate boolean. A fact only qualifies as a finding if it is measurably RARE in correct work — see `observaciones` for the ones that are not.
        - `eje` string
        - `clase` 'ventanas_sin_solape' | 'premisa_derogada' | 'premisa_no_nacida' | 'fuera_de_fecha' | 'fuera_de_territorio' | 'sin_autoridad_vinculante'
        - `citas` string[]
        - `detalle` string
        - `no_afirma` string: What the finding does NOT assert. Travels with it so it cannot be quoted alone as a verdict.
      - `observaciones` object[]: FACTS ABOUT THE SET THAT ARE NOT ANOMALIES, each carrying its measured base rate. Citing authority from another British forum fires on 49.71% of real Scottish and Northern Irish judgments, and in 18.21% every authority cited is from another forum (7,494 judgments, 23,334 resolved citations, measured 2026-08-15) — it is normal practice, so reporting it as a finding would train integrators to ignore the findings that matter. Treat these as checklist items, not defects. `hallazgos: []` WITH a populated `observaciones` is a NORMAL result, not a clean one.
        - `eje` string
        - `clase` 'autoridad_de_otro_foro'
        - `citas` string[]
        - `detalle` string
        - `base_medida` string: How often this appears in CORRECT work, with the measurement behind it.
        - `no_afirma` string
      - `no_comprobado` object[]: Axes not examined and why. `validez_logica` is permanently here and is NOT decidable; the rest appear only when the data for them was absent, each with its reason.
        - `eje` string
        - `motivo` string
      - `sin_dato` object[]: Citations left out of a comparison for lack of data. A missing datum never becomes a finding.
        - `eje` string
        - `citas` string[]
    - `total` integer, required: Number of verifiable citations detected.
    - `error` boolean: ADDITIVE and optional; present (and always `true`) ONLY when the deterministic verification COULD NOT RUN at all (exception, timeout, corpus unreachable). READ IT BEFORE `anyBroken` or `counts`: with `error: true` the summary carries `total: 0`, `anyBroken: false` and all counters at zero — that is FILLER, not a verdict. It means "the citations were NOT checked", NOT "the citations were checked and are clean". Treat every citation in the answer as unverified and review it against the official source. It is a new optional FIELD on purpose, never a new value inside the closed `status` enum, so integrators that `switch` on status do not break. Absent on every successful verification.
    - `unverifiableClaims` integer: ADDITIVE and optional (present only when > 0). Number of legal assertions written in PROSE that invoke authority without a formal, parseable citation ("as the Court held in Vega, the appeal was dismissed"). No parser extracts them, so they never appear in `checks` and never count as broken — but with `total: 0` they are the difference between "no citations" and "authority claimed with nothing to check it against". Use it for a caution, never to mark a citation verified or not_found.
    - `coverage` object: WHAT COULD BE LOOKED AT, not just what was found. Read this BEFORE `counts` or `anyBroken`. Three situations used to reach the client indistinguishable: a report with no authorities to verify, a report whose citations were all checked, and a report that ASSERTS doctrine with no recognisable citation apparatus — so nothing could be checked at all. The third one is the dangerous one and it is what any prose without citation apparatus produces.
      - `state` 'checked' | 'partially_checked' | 'no_authority' | 'unverifiable' | 'not_measured' | 'check_failed', required: `checked` = there were citations and they were contrasted (see `checks`), and nothing else in the text asserts authority. `partially_checked` = there were citations AND ALSO statements attributing holdings to courts with no checkable reference of their own. The green verdict on the citations does NOT cover those: a single verifiable citation anywhere in the document (an appendix table of authorities, say) must not be readable as a verdict on the whole body. `no_authority` = neither citations nor authority claims: there was genuinely nothing to verify. `unverifiable` = the text attributes holdings or doctrine to courts WITHOUT any checkable reference, so NOTHING was contrasted against the corpus. This is NOT a statement that the content is wrong — it is a statement that it is unverified, which is not the same as a clean report. `not_measured` = no citation found AND the report's language has no authority-claim lexicon, so the zero is a not-measured, not a "none". `check_failed` = the verifier itself could not run (mirrors `error: true`).
      - `citations` integer, required: Citations the verifier was able to look at (equals `total`).
      - `authorityClaims` integer, required: Statements attributing a holding or settled doctrine to an authority with no checkable citation in the same sentence.
      - `examples` string[]: Up to three of those statements, truncated. Present only when there are any: "there are 4" without saying which is not actionable.
      - `language` string, required: Language the report was produced in, as resolved by the platform. Decides whether a zero in `authorityClaims` is measurable.
  - `partyScreening` PartyScreening: What was actually screened, and what was not. Read `screened` and `failed` TOGETHER: a party that could not be checked is not a party that came back clean.
    - `screened` integer: Parties ACTUALLY checked against the list. Never includes the ones that failed. This field was previously named `checked` and counted both, so a partial failure reported "3 parties screened" having screened one.
    - `failed` integer: Parties that were attempted and could NOT be checked. `failed > 0` with `entries: []` is NOT a clean result.
    - `failureReason` string: Why it failed, verbatim from your provider (e.g. `HTTP 401`), so you can retry or tell them. Screening STOPS at the first failure: we never substitute your provider with anything of ours.
    - `list` string: The list actually queried. Absent when none was.
    - `entries` object[]: Only the hits. A clean screening has an empty array AND `failed: 0` — check both.
      - `entityName` string
      - `status` 'SANCTIONED' | 'POTENTIAL_MATCH' | 'CLEAN' | 'SKIPPED': `SANCTIONED` means a NAME matched at ≥0.80. It never means the party is sanctioned: secondary identifiers were not checked.
      - `score` number
      - `matchedList` string
      - `matchedName` string: The name that matched. May be an ALIAS.
      - `primaryName` string: The entity's primary name when `matchedName` is an alias. Without it you cannot adjudicate: OFAC lists «HIZBALLAH» as the entity and «LEBANESE HEZBOLLAH» among its aliases.
      - `provider` 'local' | 'byo', nullable: Who actually screened. Was previously reported as `sdn`/`opensanctions` regardless of the truth: a match from a CSV on our own disk was labelled OFAC SDN.
  - `firmMemory` FirmMemoryApplication: Whether the firm's standing preferences (Firm Memory: tone, risk criteria, model clauses, report structure — the entries managed at `/api/v1/firm-memory`) were applied to THIS analysis. READ IT BEFORE ASSUMING THEY WERE: until 2026-07-26 they were injected only by the web analysis route, so an analysis requested over REST never applied them — while the setup screen promised that every Nexus analysis would. That gap is now closed and this field is how you can tell. The firm is derived from the API key / session that signs the request. A `firm_id` in the request body is neither read nor honoured: firm scope is never client-supplied. ADDITIVE and optional, with its own enum — never a new value inside an already-published one. ABSENT when the request carried no account holder, in which case the response makes no claim either way.
    - `status` 'applied' | 'none' | 'unavailable', required: `applied` — preferences were read and injected into the Node A prompt. `none` — they were read and this firm has none for the scope of this analysis (jurisdiction + legal branch + global). `unavailable` — they COULD NOT BE READ, and the report was produced WITHOUT them. This is NOT `none`: it does not mean the firm has no preferences, it means we do not know. The analysis is still returned and still charged (the document was processed); if the preferences matter for this document, re-run it.
    - `entries` object[]: WHICH standing instructions were applied, one by one. Present when `status` is `applied`. Until 2026-07-27 the injected block instructed the model not to mention or cite these preferences in its output — binding instructions the result was forbidden to declare. That order has been removed: preferences still must not appear as meta-commentary in the body of the report, but the fact that they applied, and which ones, is declared here. A firm with a compliance function cannot accept standing instructions it is unable to evidence.
      - `id` string, uuid, required
      - `title` string, required
      - `category` 'clausula' | 'tono' | 'riesgo' | 'formato' | 'general', required
      - `scope` 'global' | 'jurisdiction' | 'subject_matter' | 'case', required: How wide the entry is. `global` binds every analysis of the firm; the rest only match this analysis's jurisdiction, subject matter or case file. Relevant when auditing why a report reads the way it does.
      - `version` integer, required
    - `clausesRefused` integer: Number of firm-memory clauses REMOVED before the prompt was assembled because they asked for something firm memory does not decide. Firm memory governs HOW the analysis is written (tone, terminology, structure, emphasis, risk posture, model clauses, sections that must always appear). It cannot switch off verifications or safeguards, change the jurisdiction or the output language, suppress warnings or adverse findings, order concealment, or redefine the model's role. A non-zero value means an entry in the firm's memory tried one of those and the rest of that entry was still applied — inspect the entries at `/api/v1/firm-memory`. Absent when nothing was removed.
    - `refusedAxes` string[]: Which capability was being attempted, for each removed clause. The clause text is deliberately not returned: it may quote case facts.

## Other responses

- `400`: Invalid parameters or malformed JSON. Includes MISSING_JURISDICTION (`jurisdiction` is required — no jurisdiction is assumed by default) and INVALID_JURISDICTION (unsupported code).
- `401`: Invalid API key or insufficient permissions.
- `402`: Insufficient credits to process the request.
- `409`: Conflict. `BYO_PROVIDER_REQUIRED` — the firm is flagged by the operator as a BYO-mandatory integrator and has no BYO configuration enabled: register your provider with `POST /api/v1/llm-providers` and enable it (there is no fallback to the platform engines; fail-closed). Also returned when an `Idempotency-Key` is reused with a different body.
- `422`: Unprocessable entity. Two cases: (a) `VALIDATION_ERROR` — text outside the allowed limits; (b) `OUTPUT_WITHHELD_BY_POLICY` (type `policy_error`) — delegated verification enforcement is active for the tenant/sub-key and the output contained genuinely broken citations (mismatch/not_found/derogated). The request was valid — the block was ordered by your platform operator, not by Nexus. **No credits are charged.** `error.details` follows the `WithholdDetails` schema (policy_scope, policy_version, broken_count, broken_citations[] and a signed `attestation`).
- `429`: Rate limit exceeded.
- `500`: Internal server error.

---

[API](https://skmtc.dev/nexusquantum/apis/nexus-legal-api.md) · [All operations](https://skmtc.dev/nexusquantum/apis/nexus-legal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/nexusquantum/apis/nexus-legal-api/revisions/4084d8d4eb0b?raw)
