{
  "openapi": "3.1.0",
  "info": {
    "title": "Veyns identity and approval API",
    "version": "0.6.1",
    "description": "Controlled sandbox pilot. Backend credentials stay on application servers. Decision acknowledgement is separate from business execution."
  },
  "servers": [
    {
      "url": "https://sandbox.id.veyns.io"
    }
  ],
  "paths": {
    "/v1/authorize/prepare": {
      "post": {
        "summary": "Prepare a PKCE request without putting action details in a URL",
        "responses": {
          "201": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "authorization_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_in": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PreparedAuthorization"
              }
            }
          }
        }
      }
    },
    "/v1/approvals": {
      "post": {
        "summary": "Request palm approval for an application-linked subject",
        "responses": {
          "201": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Approval"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Setup required (scanner_required or palm_not_linked), or idempotency conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "applicationBackend": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalRequest"
              }
            }
          }
        },
        "description": " New requests require an admitted account, an active passkey, its verified palm identity and at least one active verified scanner. A 409 scanner_required response means reconnect and verify a scanner before creating a new approval. No request is created for this error. Identical idempotent retries still return an existing request after scanner removal; this does not grant approval."
      }
    },
    "/v1/approvals/{request_id}": {
      "get": {
        "summary": "Read the durable decision",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Approval"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "applicationBackend": []
          }
        ]
      },
      "parameters": [
        {
          "name": "request_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ]
    },
    "/v1/approvals/{request_id}/cancel": {
      "post": {
        "summary": "Cancel a pending request, including an active verification",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Approval"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "applicationBackend": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        }
      },
      "parameters": [
        {
          "name": "request_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ]
    },
    "/v1/approvals/{request_id}/ack": {
      "post": {
        "summary": "Acknowledge delivery using a stable business operation id",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "applicationBackend": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Acknowledgement"
              }
            }
          }
        }
      },
      "parameters": [
        {
          "name": "request_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ]
    },
    "/v1/actions/consume": {
      "post": {
        "summary": "Acknowledge an earlier browser-action receipt",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "applicationBackend": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jti",
                  "action_digest",
                  "operation_id"
                ],
                "properties": {
                  "jti": {
                    "type": "string"
                  },
                  "action_digest": {
                    "type": "string"
                  },
                  "operation_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/.well-known/openid-configuration": {
      "get": {
        "summary": "Discover the issuer and its OAuth endpoints",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/jwks.json": {
      "get": {
        "summary": "Read the issuer verification keys",
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/account/palm/pairing": {
      "post": {
        "summary": "Create a one-time scanner connection QR for the signed-in account",
        "description": "First-party account UI only. Requires recent sign-in, an active passkey, palm admission and same-origin POST. QR routing grants no palm consent. The QR is consumable for 600 seconds; verification starts a separate 300-second deadline. Existing pairing_code consumers remain compatible. Identical signed pairing retries return the original request while the code is valid.",
        "security": [
          {
            "accountSession": []
          }
        ],
        "responses": {
          "201": {
            "description": "Private connection QR; expires in 600 seconds. Never log or send it to a third-party QR renderer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "pairing_code",
                    "qr_uri",
                    "issuer",
                    "expires_in",
                    "setup_id",
                    "expires_at",
                    "server_time"
                  ],
                  "properties": {
                    "pairing_code": {
                      "type": "string"
                    },
                    "qr_uri": {
                      "type": "string",
                      "format": "uri",
                      "description": "Fixed issuer /palm#pair= followed by 20 uppercase hexadecimal characters."
                    },
                    "issuer": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expires_in": {
                      "type": "integer",
                      "const": 600
                    },
                    "setup_id": {
                      "type": "string",
                      "description": "Opaque, account-scoped setup status handle. Never use as an authentication credential."
                    },
                    "expires_at": {
                      "type": "integer",
                      "description": "QR expiry as Unix seconds."
                    },
                    "server_time": {
                      "type": "integer",
                      "description": "Current server time as Unix seconds for countdowns."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Recent account sign-in required"
          },
          "403": {
            "description": "Palm access disabled or active passkey required"
          }
        }
      }
    },
    "/v1/txn/{transaction_id}/palm": {
      "post": {
        "summary": "Begin palm sign-in for an existing PKCE ceremony",
        "description": "First-party Veyns ceremony UI. Account routing is resolved from server-issued cookies only, never from a submitted account ID. This endpoint cannot authenticate the caller.",
        "parameters": [
          {
            "name": "transaction_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "other_scanner": {
                    "type": "boolean",
                    "description": "Cancel the pending prompt and offer a new QR. Rejected once verification begins."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bound palm request. Older code entry clients remain compatible.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "request_id": {
                      "type": "string"
                    },
                    "request_code": {
                      "type": "string"
                    },
                    "delivery": {
                      "type": "string",
                      "enum": [
                        "scanner",
                        "qr"
                      ]
                    },
                    "qr_uri": {
                      "type": "string",
                      "format": "uri",
                      "description": "Fixed issuer /palm#login= plus the one-time code; display only for QR delivery."
                    },
                    "expires_at": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown or closed ceremony"
          },
          "409": {
            "description": "Previous request is already being verified"
          }
        }
      }
    },
    "/v1/txn/{transaction_id}/result": {
      "get": {
        "summary": "Check a palm sign-in ceremony",
        "description": "First-party ceremony polling with the original opaque transaction reference. Pending delivery identifies whether this browser still needs a QR scan or a connected scanner can review the request. It grants no session or approval.",
        "parameters": [
          {
            "name": "transaction_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current ceremony state. Approved responses retain the existing bound authorization-code delivery fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "delivery": {
                      "type": "string",
                      "enum": [
                        "qr",
                        "scanner"
                      ],
                      "description": "Present before approval; changes to scanner when a QR request is claimed."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Unknown palm ceremony"
          },
          "410": {
            "description": "Approved code expired"
          }
        }
      }
    },
    "/logout": {
      "get": {
        "summary": "End the current Veyns browser session",
        "description": "Ordinary sign-out ends the authentication session while retaining a delivery-only scanner preference. No sign-in or approval is granted by that preference. A different account at sign-out clears a mismatched preference.",
        "parameters": [
          {
            "name": "forget_scanner",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Explicitly forget this browser scanner preference as well as signing out."
          }
        ],
        "responses": {
          "200": {
            "description": "Signed out; authentication cookie cleared."
          },
          "302": {
            "description": "Signed out and redirected to an already registered application origin."
          }
        }
      }
    },
    "/v1/account/palm/pairing/{setup_id}": {
      "get": {
        "summary": "Check this account\u2019s scanner setup stage and deadline",
        "description": "First-party account UI only. Setup status never grants consent or exposes a capture challenge. Poll every 2.5 seconds; stop on a terminal state. Pairing expiry does not shorten an already-started verification. Removed or superseded scanner setup reports cancelled. Unknown or another account\u2019s handle returns 404. Status is retained for 20 minutes from QR creation; the QR remains consumable for only 600 seconds.",
        "security": [
          {
            "accountSession": []
          }
        ],
        "parameters": [
          {
            "name": "setup_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current setup stage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PalmSetupStatus"
                }
              }
            }
          },
          "401": {
            "description": "Account sign-in required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown setup",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Polling limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "applicationBackend": {
        "type": "http",
        "scheme": "basic",
        "description": "Registered client id and server-side application secret."
      },
      "accountSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "veyns_sid",
        "description": "First-party Veyns account session; never an application credential."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "Action": {
        "type": "object",
        "required": [
          "statement"
        ],
        "additionalProperties": false,
        "properties": {
          "statement": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "details": {
            "type": [
              "object",
              "null"
            ]
          }
        }
      },
      "Approval": {
        "type": "object",
        "required": [
          "request_id",
          "status",
          "challenge",
          "required_method",
          "expires_at"
        ],
        "properties": {
          "request_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verifying",
              "approved",
              "denied",
              "cancelled",
              "expired",
              "failed"
            ]
          },
          "challenge": {
            "type": "string"
          },
          "required_method": {
            "const": "palm"
          },
          "application": {
            "type": "string"
          },
          "kind": {
            "const": "approval"
          },
          "action": {
            "type": "object",
            "properties": {
              "statement": {
                "type": "string"
              },
              "details": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "digest": {
                "type": "string"
              }
            },
            "required": [
              "statement",
              "digest"
            ]
          },
          "created_at": {
            "type": "integer"
          },
          "expires_at": {
            "type": "integer"
          },
          "decision": {
            "type": "string"
          },
          "decision_id": {
            "type": "string"
          },
          "acknowledged": {
            "type": "boolean"
          },
          "approval_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ApprovalRequest": {
        "type": "object",
        "required": [
          "subject",
          "idempotency_key",
          "action"
        ],
        "additionalProperties": false,
        "properties": {
          "subject": {
            "type": "string"
          },
          "idempotency_key": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          },
          "expires_in": {
            "type": "integer",
            "minimum": 30,
            "maximum": 300,
            "default": 300
          },
          "action": {
            "$ref": "#/components/schemas/Action"
          }
        }
      },
      "Acknowledgement": {
        "type": "object",
        "required": [
          "decision_id",
          "operation_id"
        ],
        "additionalProperties": false,
        "properties": {
          "decision_id": {
            "type": "string"
          },
          "operation_id": {
            "type": "string",
            "minLength": 8,
            "maxLength": 128
          }
        }
      },
      "PreparedAuthorization": {
        "type": "object",
        "required": [
          "response_type",
          "client_id",
          "redirect_uri",
          "scope",
          "code_challenge",
          "code_challenge_method"
        ],
        "properties": {
          "response_type": {
            "const": "code"
          },
          "response_mode": {
            "enum": [
              "query",
              "web_message"
            ]
          },
          "client_id": {
            "type": "string"
          },
          "redirect_uri": {
            "type": "string",
            "format": "uri"
          },
          "scope": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "nonce": {
            "type": "string"
          },
          "code_challenge": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]{43}$"
          },
          "code_challenge_method": {
            "const": "S256"
          },
          "intent": {
            "enum": [
              "login",
              "presence",
              "action"
            ]
          },
          "required_method": {
            "enum": [
              "browser",
              "palm"
            ]
          },
          "action": {
            "$ref": "#/components/schemas/Action"
          }
        }
      },
      "PalmSetupStatus": {
        "type": "object",
        "required": [
          "stage",
          "status",
          "expires_at",
          "server_time"
        ],
        "properties": {
          "stage": {
            "type": "string",
            "enum": [
              "pairing",
              "verification"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verifying",
              "approved",
              "denied",
              "cancelled",
              "expired",
              "failed"
            ]
          },
          "expires_at": {
            "type": "integer",
            "description": "Deadline of the current stage, as Unix seconds."
          },
          "server_time": {
            "type": "integer"
          }
        }
      },
      "AccountId": {
        "type": "string",
        "pattern": "^acct_[A-Za-z0-9_-]{12}$",
        "description": "Current internal account handle used in staff/account tooling, generated from 9 cryptographically random bytes. Treat as opaque. This is NOT an application subject; applications must use the pairwise sub issued for their client."
      }
    }
  }
}
