{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/attestwire-com/json-schema/attestwire-com-validation-result-schema.json",
  "title": "ValidationResult",
  "x-generated": "2026-11-02",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/attestwire-com-openapi.json#/components/schemas/ValidationResult",
  "type": "object",
  "required": [
    "valid",
    "profile",
    "errors",
    "warnings",
    "information"
  ],
  "properties": {
    "valid": {
      "type": "boolean",
      "description": "True when there are no fatal errors. Neither warnings nor information findings affect it."
    },
    "profile": {
      "type": "string",
      "enum": [
        "en16931",
        "xrechnung-ubl",
        "xrechnung-cii",
        "facturx-en16931",
        "peppol-bis-3"
      ]
    },
    "errors": {
      "type": "array",
      "description": "Findings at severity `fatal`. Non-empty means `valid` is false.",
      "items": {
        "$ref": "#/$defs/TeachingError"
      }
    },
    "warnings": {
      "type": "array",
      "description": "Findings at severity `warning`: worth fixing, but the document is accepted.",
      "items": {
        "$ref": "#/$defs/TeachingError"
      }
    },
    "information": {
      "type": "array",
      "description": "Findings at severity `information` — advisory only, and kept out of `warnings` deliberately so a caller that treats warnings as a pre-ship checklist is not handed something the regulator raises and then ignores. Example: BR-DE-TMP-32, no time of supply on an XRechnung invoice.",
      "items": {
        "$ref": "#/$defs/TeachingError"
      }
    },
    "findings_truncated": {
      "$ref": "#/$defs/FindingsTruncated"
    },
    "syntax": {
      "type": "string",
      "enum": [
        "ubl",
        "cii"
      ],
      "description": "Which reader ran: `ubl` for a UBL 2.1 `Invoice`, `cii` for a UN/CEFACT `CrossIndustryInvoice`. Decided from the root element of the document you sent, never from a parameter — there is no way to ask for the wrong one. Document requests only."
    },
    "container": {
      "type": [
        "string",
        "null"
      ],
      "description": "When a Factur-X or ZUGFeRD PDF was sent: the name of the attachment the XML was read from, and judged. What the PDF gets wrong about that attachment and its XMP metadata is an AW-PDF-* finding; PDF/A conformance itself, and whether the page agrees with the XML, are not checked. `null` for an XML file. Document requests only."
    },
    "customizationId": {
      "type": [
        "string",
        "null"
      ],
      "description": "BT-24, exactly as the document states it — not as we would have written it. `profile` is derived from this. `null` when the document states none, in which case the profile was guessed and the guess is reported in `unmapped`. Document requests only."
    },
    "profileId": {
      "type": [
        "string",
        "null"
      ],
      "description": "BT-23, exactly as the document states it. `null` when the document states none. Document requests only."
    },
    "unmapped": {
      "type": "array",
      "description": "Everything in the document that did not reach the invoice model. SILENT DATA LOSS ON A TAX DOCUMENT IS THE WORST POSSIBLE FAILURE, so this is reported rather than swallowed — read it. Document requests only; `[]` when the whole document was understood.",
      "items": {
        "$ref": "#/$defs/UnmappedElement"
      }
    },
    "unmapped_truncated": {
      "$ref": "#/$defs/UnmappedTruncated"
    },
    "source": {
      "type": "string",
      "description": "Plain-English statement of what was done and what it does not prove: the document was read into the invoice model and the model was validated, which is a pre-flight and not a schematron — and, for a PDF, what of the PDF was checked (its attachment and XMP metadata) and what was not (PDF/A conformance, the page). Document requests only."
    },
    "profile_choice": {
      "$ref": "#/$defs/ProfileChoice"
    },
    "provenance": {
      "$ref": "#/$defs/Provenance"
    },
    "record": {
      "$ref": "#/$defs/RecordPointer"
    },
    "record_unavailable": {
      "$ref": "#/$defs/RecordUnavailable"
    }
  },
  "$defs": {
    "FindingsTruncated": {
      "type": "object",
      "description": "Present ONLY when a findings array was truncated, and absent — not null, not zeroed — otherwise. Each of `errors`, `warnings` and `information` is capped independently; past the cap the array is cut and this object states what was left out. `valid` still reflects the whole document: truncation changes what is printed, never the verdict. Reaching the cap takes a hundred-plus invoice lines on which every field is wrong, and at that scale the errors are systematic — every distinct problem in the document already appears in what you were given. Treat it as a hint to fix the pattern and re-validate, not as an error.",
      "required": [
        "total",
        "returned",
        "cap",
        "by_severity",
        "note"
      ],
      "properties": {
        "total": {
          "type": "integer",
          "description": "Findings the engine produced, across all three severities."
        },
        "returned": {
          "type": "integer",
          "description": "Findings actually listed in this response."
        },
        "cap": {
          "type": "integer",
          "description": "The per-severity cap this deployment applied."
        },
        "by_severity": {
          "type": "object",
          "description": "Per-array totals, so you can see which one overflowed.",
          "required": [
            "errors",
            "warnings",
            "information"
          ],
          "properties": {
            "errors": {
              "$ref": "#/$defs/TruncationCount"
            },
            "warnings": {
              "$ref": "#/$defs/TruncationCount"
            },
            "information": {
              "$ref": "#/$defs/TruncationCount"
            }
          }
        },
        "note": {
          "type": "string",
          "description": "Prose explanation, safe to show a user or an agent verbatim."
        }
      }
    },
    "KositConformance": {
      "type": "object",
      "description": "When the official KoSIT validator was last run against this engine's committed fixtures, and with what.\n\n**NOTHING IS SENT TO KoSIT WHEN YOU CALL US.** This is a dated record of a check we ran on release, not a per-call verification, and it says nothing about YOUR document.",
      "required": [
        "recorded",
        "validator",
        "configuration",
        "configuration_published"
      ],
      "properties": {
        "recorded": {
          "type": "string",
          "format": "date",
          "description": "The date of the last recorded full fixture run."
        },
        "validator": {
          "type": "string",
          "description": "Version of the KoSIT standalone validator used."
        },
        "configuration": {
          "type": "string",
          "description": "Version of the XRechnung validator configuration used."
        },
        "configuration_published": {
          "type": "string",
          "format": "date",
          "description": "The date that configuration was published by KoSIT."
        }
      }
    },
    "ProfileChoice": {
      "type": "object",
      "description": "Present only when the request said `\"profile\": \"auto\"`: the profile the API chose from the invoice's buyer, the rule that chose it, and why. Deterministic and made without any network call; POST /v1/recipient answers the same question from live registrations.",
      "required": [
        "requested",
        "profile",
        "rule",
        "reason"
      ],
      "properties": {
        "requested": {
          "type": "string",
          "enum": [
            "auto"
          ]
        },
        "profile": {
          "type": "string",
          "enum": [
            "en16931",
            "xrechnung-ubl",
            "peppol-bis-3",
            "xrechnung-cii",
            "facturx-en16931"
          ]
        },
        "rule": {
          "type": "string",
          "enum": [
            "leitweg-id",
            "belgium",
            "peppol-address",
            "france",
            "germany-austria-business",
            "default"
          ],
          "description": "`leitweg-id`: the buyer's electronic address has scheme 0204, or a German buyer reference is a Leitweg-ID. `belgium`: the buyer is in Belgium. `peppol-address`: the buyer's electronic address is a Peppol participant identifier. `france`: the buyer is in France. `germany-austria-business`: a business buyer in Germany or Austria. `default`: none of these."
        },
        "reason": {
          "type": "string",
          "description": "The rule, as a sentence about this invoice."
        }
      }
    },
    "Provenance": {
      "type": "object",
      "description": "What ran. Present on every metered response — POST /v1/validate and POST /v1/parse, JSON body or XML document, and the JSON envelope of POST /v1/generate including the `422` it returns for an invoice it will not emit.\n\nWHY IT EXISTS. Pin `@attestwire/en16931` and your CI gets the same answer for the same invoice forever; the hosted API always runs the current rule set, because a compliance service that froze its rules would be answering last quarter's regulation. Both are correct and the difference used to be invisible — a `valid: false` looked identical whether your invoice had changed or our rules had. This says which.\n\nThe one metered response WITHOUT it is `/v1/generate?format=xml`, which returns the document alone: an XML file has nowhere to put this, and this API does not write its own metadata into a customer's tax document.",
      "required": [
        "engine",
        "engine_version",
        "ruleset",
        "profile",
        "kosit_conformance"
      ],
      "properties": {
        "engine": {
          "type": "string",
          "description": "The npm package every compliance decision on this API comes from. Install this package at `engine_version` and you have the build that judged your document."
        },
        "engine_version": {
          "type": "string"
        },
        "ruleset": {
          "type": "string",
          "description": "The standard and our build of it, as one identifier to quote."
        },
        "profile": {
          "type": [
            "string",
            "null"
          ],
          "description": "The CIUS that ACTUALLY ran, read from the result rather than from your request: a body naming no profile is judged as `en16931`, and this is where you see that. `null` when there was no verdict to read it from."
        },
        "kosit_conformance": {
          "$ref": "#/$defs/KositConformance"
        }
      }
    },
    "RecordPointer": {
      "type": "object",
      "description": "Where the validation record for this call lives. Present on a 200 from POST /v1/validate when `?record=true` was sent and the record was stored.\n\nBoth URLs are public and neither takes an API key: the id is 128 random bits and is the whole access-control story. Send `url` to a human, read `json` from a machine.",
      "required": [
        "id",
        "url",
        "json",
        "expires"
      ],
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[0-9a-f]{32}$",
          "description": "128-bit record id, lowercase hex. Unguessable by design."
        },
        "url": {
          "type": "string",
          "format": "uri",
          "description": "The human-readable record page. This is the link to paste into an email."
        },
        "json": {
          "type": "string",
          "format": "uri",
          "description": "The same record as JSON — `GET /v1/records/{id}`."
        },
        "expires": {
          "type": "string",
          "format": "date-time",
          "description": "When the record is deleted. 365 days after creation, always. Not extendable and not recoverable afterwards."
        }
      }
    },
    "RecordUnavailable": {
      "type": "object",
      "description": "The validation succeeded and a record was asked for, but not stored. THE VERDICT IN THIS RESPONSE IS UNAFFECTED and is the answer you were charged for — the record is a receipt for it, not part of it. Retry by sending the same document again with `?record=true`.",
      "required": [
        "reason",
        "message"
      ],
      "properties": {
        "reason": {
          "type": "string",
          "enum": [
            "record_limit_exceeded",
            "record_store_failed",
            "records_not_enabled"
          ],
          "description": "`record_limit_exceeded` — this key has stored 500 records today; the ceiling resets at 00:00 UTC. `record_store_failed` — storage refused the write; transient, retry. `records_not_enabled` — this deployment has no record store bound."
        },
        "message": {
          "type": "string",
          "description": "Prose safe to show a user verbatim."
        }
      }
    },
    "SourceLocation": {
      "type": "object",
      "description": "Only on findings for a document, and not on an `AW-` one, which is about the whole document. The line and column of the element in the document that was sent. When the element is missing, or several could be meant and nothing says which, `exact` is false and this is the nearest enclosing element that is there.",
      "required": [
        "line",
        "column",
        "path",
        "exact"
      ],
      "properties": {
        "line": {
          "type": "integer",
          "minimum": 1
        },
        "column": {
          "type": "integer",
          "minimum": 1,
          "description": "Of the start tag, in UTF-16 code units."
        },
        "path": {
          "type": "string",
          "description": "The element's path, with the document's own prefixes."
        },
        "exact": {
          "type": "boolean"
        },
        "attachment": {
          "type": "string",
          "description": "Only when a PDF was sent: the name of the attachment the XML was read from. `line` and `column` are then in that XML, not in the PDF."
        }
      }
    },
    "TeachingError": {
      "type": "object",
      "description": "One finding, with enough context to fix the invoice without opening the specification. Almost always a rule failure. The exception is a finding the engine raises about a document as a whole, whose `rule` starts `AW-` — such as `AW-PROFILE-SUBSET`, fatal, on a Factur-X or ZUGFeRD file whose BT-24 declares MINIMUM or BASIC WL, profiles that carry too little to be an EN 16931 invoice. It comes first, ahead of the rule failures it explains. The others are about a Factur-X or ZUGFeRD PDF's container and never fatal: `AW-PDF-ATTACHMENT`, `AW-PDF-AF`, `AW-PDF-RELATIONSHIP` and `AW-PDF-MIME` on the attached invoice XML, `AW-PDF-XMP` and `AW-PDF-XMP-PROFILE` on the XMP metadata, all on field `document` except an `AW-PDF-XMP-PROFILE` that contradicts the XML's own BT-24, which is on `BT-24`. An `AW-` finding has no rules page, so no `docsUrl`, and no `xpath` or `location`: its `message` and `fix` are the whole explanation.",
      "required": [
        "rule",
        "field",
        "severity",
        "message",
        "fix"
      ],
      "properties": {
        "rule": {
          "type": "string",
          "description": "The rule id, as the KoSIT validator prints it: BR-*, BR-DE-*, PEPPOL-EN16931-R*. `ATW-*` is a check of this validator's own rather than the regulation's. `AW-*` is not a rule at all: see above."
        },
        "field": {
          "description": "Business term(s) the rule constrains.",
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          ]
        },
        "severity": {
          "type": "string",
          "enum": [
            "fatal",
            "warning",
            "information"
          ],
          "description": "The reference validators' own three levels. `fatal` rejects the document; `warning` does not; `information` is advisory — KoSIT raises it and then accepts the invoice."
        },
        "message": {
          "type": "string"
        },
        "fix": {
          "type": "string"
        },
        "xpath": {
          "type": "string",
          "description": "Where the element is. For a JSON invoice, its place in the UBL this API would generate. For an XML document, its path in that document as written when it is there, otherwise where it belongs, in the document's own syntax."
        },
        "docsUrl": {
          "type": "string",
          "format": "uri",
          "description": "Where the rule is explained. On every finding except an `AW-` one, which is not a rule and has no page."
        },
        "location": {
          "$ref": "#/$defs/SourceLocation"
        }
      }
    },
    "TruncationCount": {
      "type": "object",
      "required": [
        "total",
        "returned"
      ],
      "properties": {
        "total": {
          "type": "integer"
        },
        "returned": {
          "type": "integer"
        }
      }
    },
    "UnmappedElement": {
      "type": "object",
      "description": "One element of the document that did not reach the invoice model. `kind` separates two very different situations and you must not treat them alike.",
      "required": [
        "path",
        "name",
        "namespace",
        "kind",
        "reason"
      ],
      "properties": {
        "path": {
          "type": "string",
          "description": "Path from the root, with a `[n]` index on repeated siblings."
        },
        "name": {
          "type": "string",
          "description": "Qualified name as the document writes it."
        },
        "namespace": {
          "type": "string",
          "description": "Namespace URI of the element."
        },
        "kind": {
          "type": "string",
          "enum": [
            "unknown",
            "recomputed"
          ],
          "description": "`unknown` — there is no field for it, so THE CONTENT IS GONE from the model and from this validation. If it matters, read it out of the XML yourself. `recomputed` — the element is understood, but the model derives the value from the lines rather than storing it (line net amounts, the VAT breakdown). Nothing is lost."
        },
        "reason": {
          "type": "string",
          "description": "Why it did not reach the model."
        },
        "text": {
          "type": "string",
          "description": "Text content of the element, when it had any."
        }
      }
    },
    "UnmappedTruncated": {
      "type": "object",
      "description": "Present ONLY when the `unmapped` array was cut, and absent otherwise. One entry per invoice line is normal — line net amounts are recomputed — so a very long document produces a very long list that helps nobody. The validation itself is unaffected.",
      "required": [
        "total",
        "returned",
        "cap",
        "note"
      ],
      "properties": {
        "total": {
          "type": "integer",
          "description": "Unmapped elements the reader found."
        },
        "returned": {
          "type": "integer",
          "description": "Unmapped elements listed in this response."
        },
        "cap": {
          "type": "integer",
          "description": "The cap this deployment applied."
        },
        "note": {
          "type": "string"
        }
      }
    }
  }
}