{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/tessio-eu/json-schema/tessio-eu-age-check-schema.json",
  "title": "AgeCheck",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/tessio-eu-openapi.json#/components/schemas/AgeCheck",
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid"
    },
    "status": {
      "type": "string",
      "enum": [
        "pending",
        "completed",
        "expired",
        "failed"
      ]
    },
    "result": {
      "description": "Null until status is completed. Stays null when status is failed, which means no answer was obtained.",
      "oneOf": [
        {
          "type": "null"
        },
        {
          "$ref": "#/$defs/AgeCheckResult"
        }
      ]
    },
    "reference": {
      "type": [
        "string",
        "null"
      ],
      "description": "Your correlation id from create, echoed back"
    },
    "mode": {
      "$ref": "#/$defs/Mode"
    },
    "failedAttempts": {
      "type": "integer",
      "description": "How many presentations failed verification, or wallet error responses arrived, while the check stayed pending, on either transport. Neither ends the check, so a pending or expired check with a count above zero means a response was posted and refused, or a wallet said it could not answer. It counts every refused response for the check, not only the holder's, so read it as a signal rather than as the person's own attempts.\n"
    },
    "lastFailureCodes": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "string"
      },
      "description": "The error codes of the most recent failed attempt, such as `issuer_untrusted` or `signature_invalid`. A code beginning `wallet_` is the wallet's own error response, for example `wallet_access_denied` when the holder declined, the wallet held no matching credential or it could not authenticate the holder (OpenID4VP 1.0 section 8.5 gives one code for all three): it is the wallet's claim and nothing was verified. `session_mismatch` is a same device presentation whose redirect came back to a different browser from the one that opened the wallet, rejected under HAIP 1.0 section 5.1. Null when there has been none. Kept after the check completes.\n"
    }
  },
  "$defs": {
    "AgeCheckResult": {
      "type": "object",
      "properties": {
        "minAge": {
          "type": "integer"
        },
        "meetsMinAge": {
          "type": "boolean",
          "description": "True if the user proved they meet the age threshold"
        },
        "method": {
          "type": "string",
          "enum": [
            "demo",
            "simulated",
            "eudiw",
            "av"
          ],
          "description": "What answered the check. 'demo' is a sandbox completion and 'simulated' is the public demo's explicit simulate call, neither of which involves a wallet. 'eudiw' and 'av' name the profile a real wallet presented under."
        },
        "transport": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "openid4vp",
            "dcapi",
            null
          ],
          "description": "How a real wallet's answer travelled: `openid4vp` for the deeplink flow, `dcapi` for the W3C Digital Credentials API. Null when no wallet was involved, so a sandbox completion stays distinguishable on this field too.\n"
        }
      }
    },
    "Mode": {
      "type": "string",
      "enum": [
        "test",
        "live"
      ],
      "description": "The mode of the project that created this check, fixed when the project was created. `test` is a sandbox: the check completes on its own after a couple of seconds and its result is simulated, so `meetsMinAge` proves nothing about anyone. `live` means only a wallet presentation completes the check. Read this before acting on `meetsMinAge`.\n"
    }
  }
}