{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/contrie-com/json-schema/contrie-com-extraction-response-schema.json",
  "title": "ExtractionResponse",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/contrie-com-openapi.yml#/components/schemas/ExtractionResponse",
  "type": "object",
  "required": [
    "success",
    "metadata"
  ],
  "properties": {
    "success": {
      "type": "boolean",
      "description": "The acceptance decision. For structured output, true already means metadata.valid is true and qualityScore is at least 70; the other metadata fields explain a refusal and scope an accepted answer, they are not a second gate."
    },
    "data": {
      "description": "Extracted result matching the requested schema, or null.",
      "type": [
        "object",
        "array",
        "string",
        "number",
        "boolean",
        "null"
      ]
    },
    "markdown": {
      "type": "string",
      "description": "Present on an accepted result when format is 'markdown' or 'both'; a refused 'both' request returns neither data nor markdown. Contrie reads at most the first 100,000 characters of the page's markdown; metadata.evidenceCoverage.sourceTruncated is true when it cut."
    },
    "error": {
      "type": "object",
      "description": "Present when success is false (for example RESULT_NOT_ACCEPTED): the same code and message shape as ErrorResponse.",
      "properties": {
        "code": {
          "type": "string"
        },
        "message": {
          "type": "string"
        }
      }
    },
    "metadata": {
      "type": "object",
      "required": [
        "publicContractVersion",
        "url",
        "qualityScore",
        "latencyMs",
        "credits",
        "cached",
        "evidenceCoverage"
      ],
      "properties": {
        "url": {
          "type": "string",
          "format": "uri"
        },
        "qualityScore": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
          "description": "Scoped indicator of structure, field coverage and text support, not a probability of truth; success requires valid plus a score of at least 70. When coverageMeasured is false (a natural-language-only request) the score is at most 80."
        },
        "coverageMeasured": {
          "type": "boolean",
          "description": "True when a JSON Schema let the requested fields be counted, so completeness measures them. False for a natural-language-only request: coverage was not measured, completeness earns half credit and qualityScore is at most 80."
        },
        "valid": {
          "type": "boolean",
          "description": "Structured acceptance check: schema, requested fields and sanity checks, more than 30% of the fields in the answer populated (completeness above 0.3), and at least half of all returned values (nulls excluded) found in the page text, that is groundingCoverage.grounded at least half of groundingCoverage.total. Booleans and strings under three characters are never found, so an answer made mostly of them is not valid. Does not establish semantic correctness. Inspect success before consuming the result."
        },
        "schemaValid": {
          "type": "boolean",
          "description": "Whether the structured result satisfies the supported supplied schema. Does not establish source completeness or semantic correctness."
        },
        "completeness": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Populated leaf fraction, measured against declared fields when their shape is known. Null and empty values reduce this fraction; zero and false count as populated. Acceptance requires more than 30% of the fields in the answer to be populated: this value must be above 0.3, so an answer at or below that share is refused. Without a field shape this describes returned values only, not coverage of the request."
        },
        "requestedFields": {
          "type": "object",
          "description": "Coverage of declared schema fields in the returned objects and array items. Every declared property, including optional properties, must be returned for extraction acceptance. Explicit null acknowledges a field only if schema-valid. Absent when no field shape is measured. Does not verify how many array items exist in the source.",
          "properties": {
            "total": {
              "type": "integer",
              "minimum": 1
            },
            "returned": {
              "type": "integer",
              "minimum": 0
            },
            "missing": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 50
            }
          },
          "required": [
            "total",
            "returned",
            "missing"
          ]
        },
        "grounding": {
          "type": "number",
          "minimum": 0,
          "maximum": 1,
          "description": "Fraction of checkable extracted values found in the source text. Inspect groundingCoverage for unchecked values; presence does not prove field attribution."
        },
        "latencyMs": {
          "type": "integer",
          "description": "Processing time in milliseconds after the source was acquired. It excludes the fetch and any browser render."
        },
        "trace": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/TraceEntry"
          }
        },
        "groundingFields": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/FieldGrounding"
          },
          "description": "Per-value grounding verdicts, ungrounded entries first, capped at 50. The grounding ratio above is computed over all checked values before the cap, so it can disagree with this array on a large extraction. Absent when nothing was checkable."
        },
        "groundingCoverage": {
          "allOf": [
            {
              "$ref": "#/$defs/GroundingCoverage"
            }
          ],
          "description": "How many values the grounding ratio speaks for. Counted over every value, so unlike groundingFields it is not affected by the 50-entry cap. Present whenever page text was available, so an absent ratio cannot be read as 'verified fine'."
        },
        "binding": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "judged",
            "conflicts",
            "truncated"
          ],
          "description": "Present on accepted model answers (structured or natural-language) from current deployments; absent on markdown, page-published structured data and rejected results, and on recorded samples, replays and monitor history stored before 2026-09-28. Absence means no check ran. A deterministic check of whether each judged value (numbers, ISO dates, short digit-bearing strings) belongs to its field, read against the passages about that field. A conflict means the value is on the page but those passages state a different value; treat that field as UNCONFIRMED and inspect it. Not a truth claim, and it changes neither acceptance nor credits. judged 0 means no field could be checked.",
          "properties": {
            "judged": {
              "type": "integer",
              "minimum": 0,
              "description": "Fields that had passages to judge against. Only grounded values are judged; on a large answer only some of them (see truncated). Never less than the number of conflicts."
            },
            "conflicts": {
              "type": "array",
              "description": "Paths of fields whose passages state a different value. Paths only, from your own answer.",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "path"
                ],
                "properties": {
                  "path": {
                    "type": "string",
                    "description": "JSONPath of the field, as in groundingFields."
                  }
                }
              }
            },
            "truncated": {
              "type": "boolean",
              "description": "Some grounded values were not judged: the answer had more values than the check covers, or the check hit its work cap. A field without a conflict is then not thereby confirmed."
            }
          }
        },
        "credits": {
          "type": "integer",
          "description": "Customer credits charged: 0 for refused structured output; 1 for accepted markdown; 1, 3, 8 or 15 for accepted structured output, plus 2 for a successful browser-rendered answer. Each plan caps credits per request including the rendering surcharge: Free 3, Builder 5, Pro 17; an answer above the cap is refused with INFERENCE_BUDGET_EXCEEDED and charges 0."
        },
        "contentHash": {
          "type": "string",
          "description": "SHA-256 of the source basis. Full source text is not embedded; reproducing this hash requires the same source basis separately."
        },
        "evidence": {
          "type": "object",
          "description": "Optional Ed25519 v2 seal over the complete public response except metadata.evidence itself. Verify with POST /api/v1/evidence/verify or /evidence-verifier.mjs and a separately pinned public key.",
          "properties": {
            "version": {
              "type": "integer"
            },
            "alg": {
              "type": "string",
              "enum": [
                "Ed25519"
              ]
            },
            "keyId": {
              "type": "string"
            },
            "signedAt": {
              "type": "string",
              "format": "date-time"
            },
            "packetHash": {
              "type": "string"
            },
            "signature": {
              "type": "string"
            },
            "covers": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "cached": {
          "type": "boolean",
          "description": "True when the page bytes came from Contrie's page cache because the request allowed options.maxAge."
        },
        "fetchedAt": {
          "type": "string",
          "format": "date-time",
          "description": "Original source acquisition time, preserved when reuse is allowed."
        },
        "evidenceStatus": {
          "type": "string",
          "enum": [
            "signed",
            "unavailable",
            "not_issued_for_rejected_result"
          ],
          "description": "Whether this response carries a usable metadata.evidence seal. A successful response can report unavailable when signing is not configured."
        },
        "qualityScoreKind": {
          "type": "string",
          "enum": [
            "structural_and_textual",
            "not_applicable"
          ],
          "description": "Markdown is not_applicable and has a numeric quality placeholder of 0, not a grade."
        },
        "requestId": {
          "type": "string",
          "description": "Identifier for customer support and request correlation."
        },
        "source": {
          "type": "object",
          "description": "Source provenance. Supplied HTML is caller-provided; a recorded sample is historical. Fetched time does not prove publisher freshness.",
          "properties": {
            "mode": {
              "type": "string",
              "enum": [
                "fetched",
                "provided_html",
                "recording"
              ]
            },
            "url": {
              "type": "string"
            },
            "fetchedAt": {
              "type": "string",
              "format": "date-time",
              "description": "Time the source acquisition completed, including fresh reads without cache consent. Reuse and recovered responses preserve their original time. This is not the publisher's modification time. Omitted when acquisition time is unknown."
            },
            "observedAt": {
              "type": "string",
              "format": "date-time",
              "description": "When Contrie started this run. For a keyless recording (mode recording) it is the recording time, and fetchedAt is absent."
            },
            "rawContentHash": {
              "type": "string",
              "description": "SHA-256 (hex) of the HTML Contrie processed, encoded as UTF-8. For a valid UTF-8 page with no byte-order mark, read by a plain fetch, it matches the SHA-256 of the response body; after a browser render or a legacy-encoding decode it does not. A match shows the same page, not that any field is correct."
            }
          }
        },
        "publicContractVersion": {
          "type": "integer",
          "enum": [
            1
          ]
        },
        "evidenceCoverage": {
          "type": "object",
          "properties": {
            "sourceTextIncluded": {
              "type": "boolean",
              "enum": [
                false
              ]
            },
            "attribution": {
              "type": "string",
              "enum": [
                "unmeasured"
              ]
            },
            "sourceCompleteness": {
              "type": "string",
              "enum": [
                "unmeasured"
              ]
            },
            "sourceTruncated": {
              "type": "boolean",
              "enum": [
                true
              ],
              "description": "Present only when the checked source is known to have been shortened. Omission does not establish full page, field attribution or list coverage."
            }
          },
          "required": [
            "sourceTextIncluded",
            "attribution",
            "sourceCompleteness"
          ]
        }
      }
    }
  },
  "$defs": {
    "FieldGrounding": {
      "type": "object",
      "required": [
        "path",
        "value",
        "grounded"
      ],
      "properties": {
        "path": {
          "type": "string",
          "description": "JSONPath-style location of the value inside data, e.g. $.stories[0].title. Keys that are not plain identifiers use bracket notation with JSON-escaped quotes, e.g. $[\"price (USD)\"]; array elements are always numeric, e.g. $.items[0]."
        },
        "value": {
          "type": "string",
          "description": "The extracted value, rendered as a string."
        },
        "grounded": {
          "type": "boolean",
          "description": "Whether this value's text was located in the page text by the presence check: strings case-insensitive, with whitespace runs collapsed and the page's bold, italic and inline-code markers read as formatting (a value's own * or ` must be on the page); links as the same URL; numbers by value (31.0 matches 31); an ISO date also as the page writes it in words, inside an ISO timestamp, or as a numeric date in the day/month order the page's own dates establish; GBP, EUR and INR as their symbol beside an amount (£53.74). It shows presence, not that the text belongs to this field or entity. False means unconfirmed by this check, not disproved: a value the page presents differently can be missed."
        },
        "excerpt": {
          "type": "string",
          "description": "Page text surrounding the match, roughly 45 characters either side, with case preserved and whitespace runs collapsed. Absent when grounded is false."
        }
      }
    },
    "GroundingCoverage": {
      "type": "object",
      "required": [
        "total",
        "checkable",
        "grounded"
      ],
      "description": "How much of the data a grounding ratio speaks for.",
      "properties": {
        "total": {
          "type": "integer",
          "description": "Values that assert something; nulls are not counted. Acceptance requires grounded to be at least half of this."
        },
        "checkable": {
          "type": "integer",
          "description": "How many of those carried enough text to search for. Booleans and strings under three characters do not; numbers are checked at any length. They still count in total, as values that were not found."
        },
        "grounded": {
          "type": "integer",
          "description": "How many of the checkable values were found in the page."
        }
      }
    },
    "TraceEntry": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "stage",
        "detail"
      ],
      "properties": {
        "stage": {
          "type": "string",
          "enum": [
            "loading",
            "processing",
            "complete"
          ]
        },
        "detail": {
          "type": "string",
          "description": "Fixed public progress label; never partial output."
        }
      }
    }
  }
}