{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/apier-no/json-schema/apier-no-signed-receipt-schema.json",
  "title": "SignedReceipt",
  "description": "HMAC-signed government submission receipt. Returned by `/api/v1/actions/execute` when an action submitted to Altinn / Skatteetaten / NAV produces a receipt. Persisted to the append-only `receipts` table so a future re-verify can confirm Apier issued exactly these bytes at exactly this signing time. Frame this as an integrity receipt, not a proof of legal correctness — Apier signs the wrapper, not the upstream's truth claim.\n\nThe `government_response_raw` field carries the upstream's response VERBATIM (minimally normalised: timestamps converted from Oslo-local to ISO 8601 with explicit Europe/Oslo offset, keys case-preserved, byte-level content otherwise preserved). Apier does not interpret this payload — it is preserved for defensibility. When the canonicalised form exceeds 256KB the field carries a `LargeGovernmentResponse` pointer instead, with the full bytes on `large_government_responses` joinable via the persisted `large_response_id` column (the join is server-side; the wire form is the same union shape).\n\nThe `signature` is HMAC-SHA256 over `canonicalJSON(receipt-without-signature)` using the operator-side `RECEIPT_HMAC_SECRET`. The hashed domain INCLUDES `signature_algorithm` — flipping the algorithm tag without recomputing the signature is a downgrade attack and fails verification.",
  "x-generated": "2026-11-01",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/apier-no-openapi.json#/components/schemas/SignedReceipt",
  "type": "object",
  "required": [
    "altinn_receipt_id",
    "audit_log_id",
    "org_number",
    "filing_type",
    "rule_version",
    "correlation_id",
    "government_response_raw",
    "government_response_hash",
    "signed_at",
    "signature_algorithm",
    "signature",
    "normalization_rules_applied"
  ],
  "additionalProperties": false,
  "properties": {
    "altinn_receipt_id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256,
      "description": "Upstream-issued receipt id (Altinn / Skatteetaten / NAV). Forwarded verbatim from the gov API response."
    },
    "audit_log_id": {
      "type": "string",
      "format": "uuid",
      "description": "UUID of the `audit_log` row for the action that produced this receipt. Hard FK in the database; the receipt is always preceded by its audit row."
    },
    "org_number": {
      "type": "string",
      "pattern": "^[0-9]{9}$",
      "description": "9-digit Norwegian organisation number the receipt belongs to."
    },
    "filing_type": {
      "type": "string",
      "minLength": 1,
      "maxLength": 128,
      "description": "Stable machine-readable filing-type identifier (e.g. `mva-melding`, `a-melding`, `skattemelding`). Free TEXT in v1; a future closed enum will be additive."
    },
    "rule_version": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64,
      "description": "Rulebook version active at signing time. Frozen on the receipt so a re-verify always reasons against the original rules — not whatever Rulebook version is current at re-verification."
    },
    "correlation_id": {
      "type": "string",
      "format": "uuid",
      "description": "Request-scope UUID v4. Joins `audit_log`, `provenance_log`, `evaluation_snapshots`, and `compliance_state_events` on the same forensic axis."
    },
    "government_response_raw": {
      "description": "The exact JSON received from Altinn / Skatteetaten / NAV on submission. Minimally normalised: timestamps in Norwegian local-time form (`YYYY-MM-DD[ T]HH:mm:ss`) are converted to ISO 8601 with explicit Europe/Oslo offset (`+02:00` CEST / `+01:00` CET, DST-aware via date-fns-tz, never hardcoded). Keys are case-preserved (`MvaPeriode` stays `MvaPeriode`). All other byte-level value content is preserved. Apier does not interpret this payload — it is preserved verbatim for defensibility. When the canonicalised form is >256KB the receipt carries the spill-bucket `LargeGovernmentResponse` pointer here instead; consumers must inspect the shape via the `truncated` discriminator.\n\nThe inline branch accepts any JSON root (object, array, string, number, boolean, null) — Skatteetaten and Altinn endpoints occasionally return top-level arrays or primitives, and the lib treats `government_response_raw` as `unknown` (preserves verbatim). The branches use `anyOf` (not `oneOf`) because a `LargeGovernmentResponse` instance is also a generic object and would match BOTH branches, failing `oneOf`'s exactly-one constraint — the discriminator the consumer parses is `truncated`, not the JSON-Schema branch. When this receipt was produced by a mock submission (Apier's `ALTINN_MODE=mock` default — see the `/api/v1/actions/execute` operation description), the object carries a nested `_mock: true` boolean as the discoverable signal that no real government system was contacted.",
      "anyOf": [
        {
          "type": "object",
          "additionalProperties": true
        },
        {
          "type": "array",
          "items": {}
        },
        {
          "type": "string"
        },
        {
          "type": "number"
        },
        {
          "type": "boolean"
        },
        {
          "type": "null"
        },
        {
          "$ref": "#/$defs/LargeGovernmentResponse"
        }
      ]
    },
    "government_response_hash": {
      "type": "string",
      "pattern": "^sha256:[0-9a-f]{64}$",
      "description": "SHA-256 of `canonicalJSON(normalised payload)`, prefixed `sha256:`. Computed against the FULL normalised payload (not the truncated projection) so re-verification can rehash from `large_government_responses.full_payload`."
    },
    "signed_at": {
      "type": "string",
      "format": "date-time",
      "description": "RFC 3339 / ISO 8601 timestamp of when the receipt was signed. Caller-supplied (will pass the route's frozen time-source) so the signature is deterministic across retries."
    },
    "signature_algorithm": {
      "$ref": "#/$defs/ReceiptSignatureAlgorithm"
    },
    "signature": {
      "type": "string",
      "pattern": "^[0-9a-f]{64}$",
      "description": "HMAC-SHA256 hex (64 chars). Computed over `canonicalJSON(receipt-without-signature)` using the operator-side `RECEIPT_HMAC_SECRET`. INCLUDES `signature_algorithm` in the hashed domain — downgrade-attack defence."
    },
    "normalization_rules_applied": {
      "type": "array",
      "description": "Diagnostic record of which `normalizeGovernmentResponse` rules fired during signing. Included in the HMAC domain (part of `canonicalJSON(receipt-without-signature)`) so an intermediary cannot rewrite normalization metadata silently — tampering with this list breaks signature verification (Major). Echoed verbatim on the wire for forensic transparency.",
      "items": {
        "type": "string",
        "enum": [
          "oslo_timestamp_normalised"
        ]
      }
    }
  },
  "$defs": {
    "LargeGovernmentResponse": {
      "type": "object",
      "description": "Compact pointer the receipt row carries on `government_response_raw` when the canonicalised normalised payload exceeded 256KB and was spilled into the `large_government_responses` table. The full bytes live one JOIN away on `large_government_responses.full_payload`; this sentinel is what a forensic peek at the receipt itself sees without joining. Stable shape regardless of upstream payload structure.",
      "required": [
        "truncated",
        "byte_length",
        "inline_projection_byte_budget",
        "hash",
        "note"
      ],
      "additionalProperties": false,
      "properties": {
        "truncated": {
          "type": "boolean",
          "enum": [
            true
          ],
          "description": "Discriminator: always true. A non-truncated receipt carries the verbatim payload on `government_response_raw` instead of this sentinel."
        },
        "byte_length": {
          "type": "integer",
          "minimum": 262145,
          "description": "Byte length of the canonicalised normalised payload at signing time. Spill happens only when the canonical form is STRICTLY >256KB; exactly 256KB stays inline. Minimum 262145 (256KB + 1 byte) reflects this invariant — under-threshold payloads cannot legitimately appear in a `LargeGovernmentResponse`."
        },
        "inline_projection_byte_budget": {
          "type": "integer",
          "minimum": 1,
          "description": "Soft cap on the inline projection size (echoed for transparency). Today: 65536 (64 KB)."
        },
        "hash": {
          "type": "string",
          "pattern": "^sha256:[0-9a-f]{64}$",
          "description": "SHA-256 of canonicalJSON(normalised payload), prefixed `sha256:`. Joins to `large_government_responses.response_hash` and matches `receipts.government_response_hash`."
        },
        "note": {
          "type": "string",
          "description": "Static guidance pointing at the spill-bucket join — instructive for a human reading the row in isolation."
        }
      }
    },
    "ReceiptSignatureAlgorithm": {
      "type": "string",
      "enum": [
        "HMAC-SHA256-v1"
      ],
      "description": "Versioned algorithm tag for signed submission receipts. v1 today: HMAC-SHA256 over the canonical-JSON form of the receipt body (with the `signature` field excluded; `signature_algorithm` itself IS in the hashed domain — downgrade-attack defence). A future rotation introduces a new value (`HMAC-SHA256-v2` on secret rotation, `Ed25519-v1` on asymmetric migration); historical receipts continue to verify under the algorithm they were signed with because the verifier branches on this column at read time."
    }
  }
}