{
  "info": {
    "name": "Valitel — EUDI Wallet verification",
    "description": "Verify EU Digital Identity Wallet credentials from your own backend.\n\n## Setup\n\n1. Mint an API key on the Valitel dashboard (**Keys**), with the scopes\n   `verifications:create` and `verifications:read`. It is shown once.\n2. Open this collection's **Variables** tab and paste the key into\n   `apiKey`, in the **current value** column — current values stay on your\n   machine, initial values travel with the collection when you export it.\n3. Send **Create verification**. Its test script stores the new session id\n   in `verificationId`, so every other request is ready to run.\n\nPoint `baseUrl` somewhere else if you run Valitel yourself.\n\nDocs: https://valitel.eu/docs · OpenAPI 3.1: https://api.valitel.eu/openapi.json",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "if (!pm.collectionVariables.get(\"apiKey\")) {",
          "  console.warn(\"Set the collection variable apiKey to your own Valitel key (vk_test_... or vk_live_...). Mint one on the dashboard Keys page.\");",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.valitel.eu",
      "type": "string",
      "description": "The Valitel API origin."
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string",
      "description": "Your own API key (vk_test_... or vk_live_...). Ships empty on purpose — paste yours into the current value column, and never commit it."
    },
    {
      "key": "verificationId",
      "value": "",
      "type": "string",
      "description": "Set automatically by the Create verification test script."
    },
    {
      "key": "responseCode",
      "value": "",
      "type": "string",
      "description": "The #response_code=... fragment a wallet appends to redirect_url in a same-device flow. Paste it before sending Redeem response code."
    }
  ],
  "item": [
    {
      "name": "Verifications",
      "description": "The merchant surface. Every request inherits the collection's bearer token.",
      "item": [
        {
          "name": "Create verification (template)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications"
              ]
            },
            "description": "Creates a verification session and mints the wallet-facing `wallet_uri`.\nRequires the `verifications:create` scope.\n\n`ttl_seconds` is 60–3600 (default 300). `metadata` is opaque to Valitel and\nis echoed back on the result and on the webhook. Add `webhook_url` to have\nthe result delivered instead of polled — it must resolve to a public\naddress, and redirects are not followed.\n\nTo retry safely (e.g. after a timeout), add an optional `Idempotency-Key`\nheader of your own; resending the same key and body within 24h replays the\noriginal response instead of creating a second session.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"template\": \"age_over_18\",\n  \"ttl_seconds\": 300,\n  \"metadata\": {\n    \"order_id\": \"postman-demo\"\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "var body = pm.response.json();",
                  "if (body.id) {",
                  "  pm.collectionVariables.set(\"verificationId\", body.id);",
                  "  console.log(\"verificationId set to \" + body.id);",
                  "}",
                  "if (body.wallet_uri) {",
                  "  console.log(\"Show this to a wallet as a QR code or deep link:\");",
                  "  console.log(body.wallet_uri);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "Create verification (inline DCQL query)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications"
              ]
            },
            "description": "The alternative to `template`: send the DCQL query inline. Exactly one of\n`template` and `query` is required — sending both is `400 invalid_request`.\n\nThis is the built-in `age_over_18` query, spelled out.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"query\": {\n    \"credentials\": [\n      {\n        \"id\": \"age_over_18_mdoc\",\n        \"format\": \"mso_mdoc\",\n        \"meta\": {\n          \"doctype_value\": \"eu.europa.ec.av.1\"\n        },\n        \"claims\": [\n          {\n            \"path\": [\n              \"eu.europa.ec.av.1\",\n              \"age_over_18\"\n            ],\n            \"intent_to_retain\": false\n          }\n        ]\n      }\n    ]\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"201 Created\", function () {",
                  "  pm.response.to.have.status(201);",
                  "});",
                  "",
                  "var body = pm.response.json();",
                  "if (body.id) {",
                  "  pm.collectionVariables.set(\"verificationId\", body.id);",
                  "  console.log(\"verificationId set to \" + body.id);",
                  "}",
                  "if (body.wallet_uri) {",
                  "  console.log(\"Show this to a wallet as a QR code or deep link:\");",
                  "  console.log(body.wallet_uri);",
                  "}"
                ]
              }
            }
          ]
        },
        {
          "name": "List verifications",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications?limit=20",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "description": "1–100, default 20."
                },
                {
                  "key": "before",
                  "value": "",
                  "description": "ISO 8601 datetime. Sessions created strictly before this cursor — page by passing the oldest created_at you have seen.",
                  "disabled": true
                }
              ]
            },
            "description": "Your organisation's sessions, newest first. Requires `verifications:read`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"200 OK\", function () { pm.response.to.have.status(200); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Get verification",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications/{{verificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications",
                "{{verificationId}}"
              ]
            },
            "description": "Fetches one session. Requires `verifications:read`.\n\n`pending` until a wallet answers — `wallet_uri` and `request_uri` are\npresent only then. Once terminal the response carries `claims` and\n`credentials` instead, and `trusted: true` on a credential is the single\nbit meaning every trust check passed.\n\nAnother organisation's id and an unknown id both return the same\n`404 session_not_found`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"200 OK\", function () { pm.response.to.have.status(200); });",
                  "",
                  "var body = pm.response.json();",
                  "console.log(\"status: \" + body.status);"
                ]
              }
            }
          ]
        },
        {
          "name": "Redeem response code",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications/{{verificationId}}/redeem",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications",
                "{{verificationId}}",
                "redeem"
              ]
            },
            "description": "The same-device alternative to polling. When a session is created with\n`redirect_url`, the wallet returns the user to it with `#response_code=...`\nappended; redeem that code here for the result. Single-use, and it counts\nas a read — `verifications:read` is the scope.\n\nSet the `responseCode` collection variable first. A wrong code, a spent\ncode and an unknown session are all `404 invalid_response_code`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"response_code\": \"{{responseCode}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Erase verification",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications/{{verificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications",
                "{{verificationId}}"
              ]
            },
            "description": "Erases the session, its result and its webhook delivery log — the three\nplaces a disclosed claim can be. Requires `verifications:delete`, which is\noffered unticked when a key is minted, so a default key cannot do this.\n\n**Irreversible.** There is no soft delete and no undo. The response carries\nthe per-table row counts, measured rather than inferred. A second DELETE of\nthe same id is a 404, deliberately."
          }
        },
        {
          "name": "Usage and quota",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "usage"
              ]
            },
            "description": "Where today's creation budget stands. Requires `verifications:read`, and\nspends nothing itself — creating a verification is the only metered call.\n\nThe budget resets at midnight UTC. Once `remaining` hits 0, creating\nanswers `429 quota_exceeded` with a `Retry-After` header — a different\ncode from `rate_limit_exceeded`, because retrying before the reset cannot\nsucceed."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"200 OK\", function () { pm.response.to.have.status(200); });",
                  "",
                  "var body = pm.response.json();",
                  "console.log(body.verifications_created + \" of \" + body.daily_quota + \" used today\");"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Public",
      "description": "No API key. Safe to call from a browser.",
      "item": [
        {
          "name": "Verification status",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/verifications/{{verificationId}}/status",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "verifications",
                "{{verificationId}}",
                "status"
              ]
            },
            "description": "The poll the drop-in widget makes from the merchant's own page. No bearer\ntoken, and deliberately limited to `{id, status}` — a browser holding only\na session id must never be able to read verified claims.\n\nSets `access-control-allow-origin: *`; it is the only cross-origin-readable\nroute in the API."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"200 OK\", function () { pm.response.to.have.status(200); });",
                  "pm.test(\"claims are not exposed\", function () {",
                  "  pm.expect(pm.response.json()).to.not.have.property(\"claims\");",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "OpenAPI document",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/openapi.json",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "openapi.json"
              ]
            },
            "description": "The generated OpenAPI 3.1 document — the machine-readable source of truth for every schema in this collection."
          }
        }
      ]
    },
    {
      "name": "Wallet (reference only)",
      "description": "What a wallet sends, documented so you can recognise it in a trace.\n\nSending these from Postman will not complete a verification: you do not hold\nthe credential, and the response body has to be a real presentation. They are\nunauthenticated because the unguessable session id in the path is the\ncapability.",
      "item": [
        {
          "name": "Fetch the request object (JAR)",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/wallet/request/{{verificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "wallet",
                "request",
                "{{verificationId}}"
              ]
            },
            "description": "Returns the authorization request object as a raw compact JWT with `Content-Type: application/oauth-authz-req+jwt` — not JSON. This is what `request_uri` inside `wallet_uri` points at."
          }
        },
        {
          "name": "Post the presentation",
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/x-www-form-urlencoded"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/wallet/response/{{verificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "wallet",
                "response",
                "{{verificationId}}"
              ]
            },
            "description": "The wallet's presentation, form-encoded — either a plaintext `direct_post` body or an encrypted `direct_post.jwt` JWE, per the configured response mode. A session accepts exactly one response; a replay is `409`.",
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                {
                  "key": "response",
                  "value": "",
                  "type": "text",
                  "description": "The encrypted JWE, for direct_post.jwt.",
                  "disabled": true
                },
                {
                  "key": "vp_token",
                  "value": "",
                  "type": "text",
                  "description": "The presentation, for plaintext direct_post.",
                  "disabled": true
                }
              ]
            }
          }
        }
      ]
    }
  ]
}
