{
  "openapi": "3.1.0",
  "info": {
    "title": "Sidaxis API",
    "version": "1.0.0-beta",
    "summary": "Proof that a live human authorised a machine.",
    "description": "Sidaxis is the human authorisation layer. Two primitives:\n\n**oneface** proves a real, living person is present, on their own device. Nothing is\nstored: no image, no template. Speech correlated with the pulse read by the camera at\nthe same instant proves a living person is there at that moment. **Voice is never used\nto say who the person is.**\n\n**Mandates** record what that person authorised a machine to do — scope, ceiling,\nexpiry — and the layer refuses anything outside it.\n\nSidaxis never moves money, holds funds or settles. It answers whether an action was\nauthorised and records that the answer was given. The executor is your own system or\nyour PSP.\n\n## Shape of the API\n\nEvery endpoint is a function under one base path:\n\n```\nhttps://api.sidaxis.com/functions/v1/<function-name>\n```\n\nThere is no `/v1` resource façade today. Function names are the contract.\n\n## Authentication\n\n`x-sidaxis-api-key: sk_live_… | sk_test_… | pk_live_… | pk_test_…`\n\nThe legacy header `x-sieve-api-key` is accepted with identical semantics. When both are\npresent, `x-sidaxis-api-key` wins. Some reads accept a first-party user JWT via\n`Authorization: Bearer` instead.\n\n- `pk_*` (publishable, browser) reaches `face-identify`,\n  `process-biovital-enrollment`, `handoff-register-public`, `merchant-theme-get`,\n  `alias-availability`.\n- `sk_*` (secret, server) reaches everything else. A `pk_*` on a server endpoint is\n  `401 AUTH_REQUIRED`.\n\nSandbox is not a separate host. Sandbox is the **merchant** plus the **key prefix**\n(`sk_test_`, `pk_test_`). Isolation is by `merchant_id`.\n\n## Refusals are not errors\n\nA refusal is `HTTP 200` with `allowed: false` and three message layers: a stable\nmachine `reason`, an English `developer_message`, and a localised `user_message`\n(absent on integration errors, by design). Malformed requests and authentication\nfailures are real HTTP errors shaped `{ \"error\": \"...\", \"code\": \"...\" }`.\n\n## check versus consume\n\n**`mandate-check` is advisory.** No lock, nothing reserved, nothing debited. It can\nreturn a false positive under concurrency; that is accepted and documented.\n\n**`mandate-consume` is authoritative.** It is the only call that moves the ceiling, it\ndoes so under `SELECT … FOR UPDATE`, and it **requires** an `Idempotency-Key` header.\n\n## Money\n\nAlways an integer in the minor unit, on the way in and on the way out. `4000` is forty\ndollars. A float is refused.\n\n## Agents\n\nThe MCP server is in production at\n`https://api.sidaxis.com/functions/v1/mcp` (Streamable HTTP, OAuth 2.1 with dynamic\nclient registration). Nine tools, each mapped one-to-one onto an endpoint documented\nhere: the six mandate calls, public receipt verification, and the two One Face calls an\nagent uses to ask a person to prove she is present. Face and voice capture are not\ntools, and never will be — the descriptor is produced on the person's own device.\n\n## Not published as a promise\n\nDated versioning (`Sidaxis-Version`), numeric rate-limit headers on every endpoint,\nper-region SLAs and cursor pagination are intentions, not contract, and are absent from\nthis document until they answer. Everything described in this document is live.\n",
    "contact": {
      "name": "Sidaxis engineering",
      "url": "https://www.sidaxis.com/contact",
      "email": "engineering@sidaxis.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://www.sidaxis.com/legal"
    }
  },
  "servers": [
    {
      "url": "https://api.sidaxis.com/functions/v1",
      "description": "Production and sandbox — the environment is the key prefix, not the host."
    }
  ],
  "x-environments": {
    "isolation": "Same host, same tables. Isolation is by merchant_id, and the environment is the key\nprefix. There is no sandbox.api.sidaxis.com.\n",
    "scenarios": {
      "header": "x-sidaxis-scenario",
      "note": "Sandbox keys only. Forces a failure path; `success` injects nothing — the real engine runs and needs a real face. The sandbox never fabricates an approval.",
      "values": [
        "success",
        "liveness_fail",
        "vrc_coerced",
        "subject_mismatch",
        "duplicate_enroll"
      ]
    },
    "differences": [
      "no_blockchain_anchoring_so_receipts_have_no_public_seal"
    ]
  },
  "security": [
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Identity",
      "description": "Prove a live human is present, and recognise them again."
    },
    {
      "name": "Mandates",
      "description": "Record and enforce what a verified human authorised a machine to do."
    },
    {
      "name": "Receipts",
      "description": "Proof that an action happened, with the strength of the proof recorded on it."
    },
    {
      "name": "Hosted",
      "description": "A single-use session token your server mints and our server exchanges when the hosted screen boots."
    },
    {
      "name": "Surface",
      "description": "Appearance and handle availability for a capture screen."
    },
    {
      "name": "One Face",
      "description": "A single-use verification card an agent hands to a person, and the result it reads back."
    },
    {
      "name": "MCP",
      "description": "The MCP server — nine tools, each mapped one-to-one onto a live endpoint."
    },
    {
      "name": "Webhooks",
      "description": "Signed events, delivered by an outbox worker every minute."
    }
  ],
  "paths": {
    "/face-identify": {
      "post": {
        "tags": [
          "Identity"
        ],
        "operationId": "faceIdentify",
        "summary": "Is this face already known?",
        "description": "Browser-safe (`pk_*`). Answers before a flow branches. The device sends only a\nderived descriptor — never video, never frames, never audio.\n\n`subject_ref` is `HMAC(sieve_id, merchant_id)`: a per-merchant pseudonym, never the\nreal identifier. The match gate is 0.75.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "face_template",
                  "capture_nonce"
                ],
                "properties": {
                  "face_template": {
                    "$ref": "#/components/schemas/FaceTemplate"
                  },
                  "capture_nonce": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answered.",
            "content": {
              "application/json": {
                "examples": {
                  "matched": {
                    "value": {
                      "matched": true,
                      "subject_ref": "sub_9f41c7b2",
                      "confidence": 0.83
                    }
                  },
                  "unmatched": {
                    "value": {
                      "matched": false,
                      "confidence": 0.41
                    }
                  }
                },
                "schema": {
                  "type": "object",
                  "properties": {
                    "matched": {
                      "type": "boolean"
                    },
                    "subject_ref": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "confidence": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`TEMPLATE_STORE_UNAVAILABLE`."
          }
        }
      }
    },
    "/process-biovital-enrollment": {
      "post": {
        "tags": [
          "Identity"
        ],
        "operationId": "enrollIdentity",
        "summary": "Create the identity",
        "description": "Browser-safe (`pk_*`). Creates the identity, or recognises the face if it already\nexists. Consent is mandatory and there is no bypass flag.\n\nThe account is born with **face and voice**. No document is asked for here.\n\n`user_id` is minted by the engine — the `auth.users` UUID. A surface never supplies\nor chooses one. An alias is only a public handle attached to that identity.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnrollmentCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enrolled.",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "status": "enrolled",
                  "subject_ref": "sub_9f41c7b2",
                  "session_token_hash": "…",
                  "consent": {
                    "version": "v1",
                    "retention_days": 7
                  }
                }
              }
            }
          },
          "400": {
            "description": "`INVALID_INPUT` — carries `fields`."
          },
          "403": {
            "description": "`CONSENT_REQUIRED` · `BIOMETRIC_LIVENESS_FAILED`."
          },
          "409": {
            "description": "`SESSION_PRESENT` · `IDENTITY_UNREACHABLE`."
          },
          "422": {
            "description": "`FACE_TEMPLATE_REQUIRED`."
          },
          "500": {
            "description": "`IDENTITY_CREATE_FAILED` · `TEMPLATE_STORE_FAILED` · `SESSION_MINT_FAILED`."
          }
        }
      }
    },
    "/process-biovital-transaction": {
      "post": {
        "tags": [
          "Identity"
        ],
        "operationId": "authoriseAction",
        "summary": "Authorise an action with a live capture",
        "description": "Server key or first-party user JWT. The authorisation sink: `ACTION_LOGIN`,\n`ACTION_PAYMENT`, `ACTION_SIGN`, `ACTION_SIGNUP`.\n\nThe row it writes is the `capture_id` that `mandate-issue` accepts as proof, valid\nfor **five minutes**.\n\n`assurance_level` is derived here, at the instant of the act, and frozen on the\nreceipt. It is never sent by the client and there is no identity-level assurance.\n\n`mandate_id` is optional. Absent, the call behaves exactly as it always has.\nPresent, the mandate stands in for the live capture and the response gains a\n`mandate` block.\n\n`ACTION_PAYMENT` and `ACTION_SIGN` require a valid document record on the identity\n— see `403 DOCUMENT_REQUIRED`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKeyOptional"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransactionCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authorised. A replayed `Idempotency-Key` returns the original body with `Idempotency-Status: replayed`.",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "status": "Payment Authorized",
                  "transaction_id": "3e976a4a-4186-49e8-bd78-b604a2f36ed3",
                  "sieve_hash": "0x…",
                  "immutability_seal": "SEAL-…",
                  "mandate": {
                    "mandate_id": "mnd_live_b8158ce8513523e91f74c6bc5a0a13e8",
                    "consumption_id": "…",
                    "scope_used": "purchase",
                    "granted_to": "agent_shopping_assistant",
                    "agent_display_name": "your travel assistant",
                    "agent_descriptor": "Books flights within your limits",
                    "assurance_level": "face_voice",
                    "idempotent_replay": false,
                    "remaining": {
                      "amount": 3990,
                      "currency": "BRL"
                    },
                    "at_limit": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `sieve_hash` or `user_id`; `AMOUNT_INVALID` when money is not an integer in the minor unit."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`DOCUMENT_REQUIRED` · `IDENTITY_MISMATCH` · `FACE_MATCH_BELOW_THRESHOLD` ·\n`COERCION_DETECTED` · `UNAUTHORIZED_DEVICE` · `ACCESS_REVOKED`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentRequired"
                }
              }
            }
          },
          "409": {
            "description": "Replay or nonce reuse."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`SLO_BREACH_PAUSED` — the circuit breaker paused this function."
          }
        }
      }
    },
    "/disclose-claims": {
      "post": {
        "tags": [
          "Identity"
        ],
        "operationId": "discloseClaims",
        "summary": "Release an attribute, or prove one without revealing it",
        "description": "Server key. Resolves each requested field into a plain `value`, a `zk_proof`\n(a derived boolean claim such as `age_over_21: true`, with the raw date of birth\nnever leaving the server), or a `document` behind a consent-token envelope.\n\nEnds with an atomic write to the consent ledger. The merchant validates the\ndisclosure with `consent_token`; `revoke-consent` kills the token and the fetch\nturns into a 403.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "zk_proof_audit_id",
                  "merchant_id",
                  "transaction_type",
                  "requestedFields",
                  "biometric_attestation"
                ],
                "properties": {
                  "zk_proof_audit_id": {
                    "type": "string"
                  },
                  "merchant_id": {
                    "type": "string"
                  },
                  "transaction_type": {
                    "type": "string"
                  },
                  "requestedFields": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "key",
                        "disclosure_type"
                      ],
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "disclosure_type": {
                          "type": "string",
                          "enum": [
                            "value",
                            "zk_proof",
                            "document"
                          ]
                        },
                        "requires_kyc": {
                          "type": "boolean"
                        },
                        "circuit_id": {
                          "type": "string"
                        },
                        "public_input": {
                          "type": "object"
                        }
                      }
                    }
                  },
                  "biometric_attestation": {
                    "type": "object",
                    "properties": {
                      "liveness_passed": {
                        "type": "boolean"
                      },
                      "vrc_confidence": {
                        "type": "number"
                      },
                      "device_pubkey_pem": {
                        "type": "string"
                      }
                    }
                  },
                  "ttl_seconds": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Disclosed, with a consent token."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Consent missing or revoked."
          }
        }
      }
    },
    "/mandate-scopes": {
      "get": {
        "tags": [
          "Mandates"
        ],
        "operationId": "mandateScopes",
        "summary": "The closed scope vocabulary, with human labels",
        "description": "Public and cacheable — it carries no subject data. Labels come from the engine in\n`en`, `pt-BR` and `es` (`Accept-Language`, silent English fallback), so every\nclient's consent screen reads identically instead of each one writing its own.\n\n`scope` and every other field name are always English and never localised. Only\n`label` and `description` change with the header. The two examples below are the\nsame call with two different `Accept-Language` values.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          }
        ],
        "responses": {
          "200": {
            "description": "The catalogue.",
            "content": {
              "application/json": {
                "examples": {
                  "english": {
                    "summary": "Accept-Language: en (default)",
                    "value": {
                      "version": "v2",
                      "locale": "en",
                      "matching": "exact",
                      "money_scopes": [
                        "purchase",
                        "payment"
                      ],
                      "constraint_required_scopes": [
                        "task"
                      ],
                      "scopes": [
                        {
                          "scope": "login",
                          "accepts_amount_ceiling": false,
                          "requires_constraints": false,
                          "label": "Sign in",
                          "description": "Sign in on your behalf."
                        }
                      ]
                    }
                  },
                  "portuguese": {
                    "summary": "Accept-Language: pt-BR — labels localised, field names and scope ids unchanged",
                    "value": {
                      "version": "v2",
                      "locale": "pt-BR",
                      "matching": "exact",
                      "money_scopes": [
                        "purchase",
                        "payment"
                      ],
                      "constraint_required_scopes": [
                        "task"
                      ],
                      "scopes": [
                        {
                          "scope": "login",
                          "accepts_amount_ceiling": false,
                          "requires_constraints": false,
                          "label": "Entrar",
                          "description": "Entrar em seu nome."
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "`METHOD_NOT_ALLOWED`."
          }
        }
      }
    },
    "/mandate-issue": {
      "post": {
        "tags": [
          "Mandates"
        ],
        "operationId": "mandateIssue",
        "summary": "Issue a mandate",
        "description": "Requires a capture no older than **five minutes**, belonging to the principal, with\nliveness passed and no coercion signal.\n\n`assurance_level` is read from the level **frozen on that capture** — never sent by\nthe client. A capture that included a verified document yields `face_voice_doc`, so\na ten-thousand mandate cannot rest on the same proof as a hundred one.\n\nA mandate is **immutable**. There is no update endpoint. Only `status`,\n`revoked_at` and `updated_at` ever change. Need more room? Issue a new mandate with\n`step_up_of` pointing at the old one, so an old receipt always describes an object\nthat never changed. The standing mandate's balance is untouched by a step-up.\n\nA money scope (`purchase`, `payment`) requires a valid document record on the\nidentity — `403 DOCUMENT_REQUIRED` otherwise.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MandateCreate"
              },
              "examples": {
                "standingPayment": {
                  "summary": "A standing payment mandate with a daily ceiling",
                  "value": {
                    "granted_by": "5099c9cd-4a97-4c4d-bcd8-79d1f8c68ae4",
                    "granted_to": "agent_shopping_assistant",
                    "granted_to_kind": "agent",
                    "agent_display_name": "your travel assistant",
                    "agent_descriptor": "Books flights and hotels within your limits",
                    "scopes": [
                      "purchase"
                    ],
                    "constraints": {
                      "merchant_allowlist": [
                        "rostopay"
                      ]
                    },
                    "ceiling": {
                      "kind": "amount",
                      "amount": 4000,
                      "currency": "USD",
                      "period": "daily"
                    },
                    "expires_at": "2026-12-31T00:00:00Z",
                    "proof": {
                      "capture_id": "3e976a4a-4186-49e8-bd78-b604a2f36ed3"
                    }
                  }
                },
                "constrainedTask": {
                  "summary": "A task mandate — constraints are mandatory",
                  "value": {
                    "granted_by": "5099c9cd-4a97-4c4d-bcd8-79d1f8c68ae4",
                    "granted_to": "agent_ops_bot",
                    "scopes": [
                      "task"
                    ],
                    "constraints": {
                      "merchant_allowlist": [
                        "rostopay"
                      ],
                      "country_allowlist": [
                        "BR"
                      ]
                    },
                    "ceiling": {
                      "kind": "count",
                      "count": 20,
                      "period": "weekly"
                    },
                    "expires_at": "2026-10-31T00:00:00Z",
                    "proof": {
                      "capture_id": "3e976a4a-4186-49e8-bd78-b604a2f36ed3"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Mandate issued. `scope_labels` comes ready in the caller's locale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Mandate"
                }
              }
            }
          },
          "400": {
            "description": "`UNKNOWN_SCOPE` · `WILDCARD_SCOPE_FORBIDDEN` · `AMOUNT_CEILING_NOT_APPLICABLE` ·\n`CONSTRAINTS_REQUIRED_FOR_SCOPE` · `AGENT_DISPLAY_NAME_TOO_LONG` ·\n`AGENT_DESCRIPTOR_TOO_LONG` · `AGENT_DISPLAY_NAME_EMPTY` ·\n`AGENT_DESCRIPTOR_EMPTY`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransportError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "`DOCUMENT_REQUIRED` on a money scope with no valid document record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentRequired"
                }
              }
            }
          }
        }
      }
    },
    "/mandate-check": {
      "post": {
        "tags": [
          "Mandates"
        ],
        "operationId": "mandateCheck",
        "summary": "Advisory — does this action fit?",
        "description": "**Advisory only.** No `Idempotency-Key`, no lock, nothing reserved, nothing\ndebited. It **can** return a false positive under concurrency; that is accepted and\ndocumented. A reservation would need expiry, release and an orphan collector —\nthree mechanisms for a problem the idempotency key on `mandate-consume` already\nsolves.\n\nNever treat an allowed check as authorisation to act without consuming. Rate\nlimited.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MandateActionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answered. `allowed: false` is a successful call, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MandateAnswer"
                },
                "examples": {
                  "allowed": {
                    "value": {
                      "allowed": true,
                      "remaining": {
                        "amount": 3000,
                        "currency": "USD"
                      },
                      "period_resets_at": "2026-09-16T00:00:00+00:00"
                    }
                  },
                  "refused": {
                    "value": {
                      "allowed": false,
                      "reason": "ceiling_exceeded",
                      "developer_message": "Consumption would exceed the mandate ceiling for the current period.",
                      "user_message": "This agent has reached its limit for the period.",
                      "step_up_available": true,
                      "remaining": {
                        "amount": 0,
                        "currency": "USD"
                      },
                      "period_resets_at": "2026-09-16T00:00:00+00:00"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/mandate-consume": {
      "post": {
        "tags": [
          "Mandates"
        ],
        "operationId": "mandateConsume",
        "summary": "Authoritative — consume against the mandate",
        "description": "The **only** call that moves the ceiling. All arithmetic happens inside\n`mandate_consume_atomic()`, which takes `SELECT … FOR UPDATE` on the mandate row\n**before** summing consumptions. Reading the balance unlocked and writing\nafterwards lets two agents through and blows the ceiling — that is the bug this\ndesign exists to prevent.\n\n`Idempotency-Key` is **mandatory**. Without it a network timeout becomes a double\ncharge, and the call is refused with `idempotency_key_required`. The same key\nreplayed returns the original consumption with `idempotent_replay: true` and debits\nonce.\n\nConsumptions are append-only: there is no balance column, because a counter loses\nthe reason and the reason is what an audit needs. The event is written to the\noutbox in the same transaction.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKeyRequired"
          },
          {
            "$ref": "#/components/parameters/Timestamp"
          },
          {
            "$ref": "#/components/parameters/Nonce"
          },
          {
            "$ref": "#/components/parameters/AcceptLanguage"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MandateActionRequest"
              },
              "example": {
                "mandate_id": "mnd_live_b8158ce8513523e91f74c6bc5a0a13e8",
                "scope": "purchase",
                "amount": 1000,
                "currency": "USD",
                "action_ref": "order_4471",
                "context": {
                  "merchant": "rostopay",
                  "country": "US"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Consumed, or refused. Both are 200.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MandateAnswer"
                },
                "examples": {
                  "consumed": {
                    "value": {
                      "allowed": true,
                      "consumption_id": "c1f0…",
                      "idempotent_replay": false,
                      "remaining": {
                        "amount": 3000,
                        "currency": "USD"
                      },
                      "at_limit": false,
                      "period_resets_at": "2026-09-16T00:00:00+00:00"
                    }
                  },
                  "missingKey": {
                    "value": {
                      "allowed": false,
                      "reason": "idempotency_key_required",
                      "developer_message": "POST /consume requires an Idempotency-Key header. Without it a network timeout becomes a double charge."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/mandate-get": {
      "get": {
        "tags": [
          "Mandates"
        ],
        "operationId": "mandateGet",
        "summary": "Read a mandate, or list them by status",
        "description": "Authorisation on **every** read. Unknown and unauthorised answer identically, so no\none can probe for existence.\n\nThe response reports the **effective** status: `expires_at` compared against the\nserver clock at read time is the authority, so a mandate reads `expired` even\nbefore the tidy-up job runs. `stored_status` shows the untouched row.\n\nA one-mandate read includes the last 100 consumptions — the statement, free. There\nis no cursor pagination today.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "One mandate."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "consumed",
                "exhausted",
                "expired",
                "revoked"
              ]
            },
            "description": "A list by effective status."
          }
        ],
        "responses": {
          "200": {
            "description": "The mandate and its consumptions, or the list.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Mandate"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "mandates": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Mandate"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "`not_found` — unknown or not visible to the caller. Identical either way, on purpose."
          }
        }
      }
    },
    "/mandate-revoke": {
      "post": {
        "tags": [
          "Mandates"
        ],
        "operationId": "mandateRevoke",
        "summary": "Revoke a mandate",
        "description": "Immediate and never cached: the next action fails with no window. `revoked` is\nterminal; nothing returns to `active`. Callable by the granting human, the issuing\nmerchant or the service role — **not** by the agent holding it. Repeat calls answer\n`already_revoked: true`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mandate_id"
                ],
                "properties": {
                  "mandate_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked, or already revoked.",
            "content": {
              "application/json": {
                "example": {
                  "revoked": true,
                  "already_revoked": false,
                  "mandate_id": "mnd_live_b815…",
                  "status": "revoked",
                  "revoked_at": "2026-09-15T22:41:02Z"
                }
              }
            }
          }
        }
      }
    },
    "/hosted-session-create": {
      "post": {
        "tags": [
          "Hosted"
        ],
        "operationId": "hostedSessionCreate",
        "summary": "Mint a hosted capture session",
        "description": "Called by **your server** with a secret key. A publishable `pk_*` is rejected:\nminting a session is a server act.\n\nThe session — not the URL — carries merchant, action, reference and amount, and it\nis what decides the values the screen shows. A browser cannot tamper with an amount\nit never carries.\n\nThere is **no redirect URL** in this contract. You hand `session_token` to the\nbrowser and open the hosted screen with it. Branding is not sent here: the screen\nreads it from `merchant-theme-get`.\n\nSingle use, 32 random bytes, returned exactly once — only `SHA-256(token)` is\npersisted, so the plaintext is never stored, logged or recoverable. Default TTL\n300 s, minimum 60, maximum 900.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HostedSessionCreate"
              },
              "example": {
                "action": "authorize",
                "external_txn_ref": "ord_10231",
                "amount_cents": 4990,
                "currency": "USD",
                "brand": "Sidaxis",
                "ttl_seconds": 300,
                "metadata": {
                  "locale": "pt-BR"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session minted. The token is shown once.",
            "content": {
              "application/json": {
                "example": {
                  "session_token": "hs_test_4ad6…0a17",
                  "session_id": "a87f81d8-…",
                  "merchant_id": "sidaxis-surface-sandbox",
                  "action": "authorize",
                  "external_txn_ref": "ord_10231",
                  "amount_cents": 4990,
                  "currency": "USD",
                  "brand": "Sidaxis",
                  "expires_at": "2026-09-15T22:44:39.349Z",
                  "ttl_seconds": 300,
                  "single_use": true
                }
              }
            }
          },
          "400": {
            "description": "`INVALID_ACTION` · `INVALID_EXTERNAL_TXN_REF` · `INVALID_AMOUNT_CENTS` ·\n`INVALID_CURRENCY` · `AMOUNT_AND_REF_REQUIRED` · `INVALID_TTL` ·\n`INVALID_BRAND` · `INVALID_METADATA` · `METADATA_TOO_LARGE`.\n"
          },
          "401": {
            "description": "`API_KEY_REQUIRED` · `INVALID_API_KEY`."
          },
          "403": {
            "description": "`MERCHANT_MISMATCH`."
          },
          "500": {
            "description": "`SESSION_WRITE_FAILED`."
          }
        }
      }
    },
    "/hosted-session-consume": {
      "post": {
        "tags": [
          "Hosted"
        ],
        "operationId": "hostedSessionConsume",
        "summary": "Exchange the token for the session",
        "description": "Called by **our server** when the hosted screen boots — service role, or the\nmerchant's own secret key. Publishable keys rejected.\n\nThe exchange is one atomic statement, so a replay can never win a second exchange.\nA refusal is **200 with a reason, never an error**. A malformed token answers\n`SESSION_NOT_FOUND` so an attacker learns nothing, and a token offered under the\nwrong merchant credentials is burned rather than left available for a second\nattempt.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_token"
                ],
                "properties": {
                  "session_token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Consumed, or refused — both 200.",
            "content": {
              "application/json": {
                "examples": {
                  "consumed": {
                    "value": {
                      "consumed": true,
                      "session": {
                        "session_id": "a87f81d8-…",
                        "merchant_id": "sidaxis-surface-sandbox",
                        "action": "authorize",
                        "external_txn_ref": "ord_10231",
                        "amount_cents": 4990,
                        "currency": "USD",
                        "brand": "Sidaxis",
                        "metadata": {},
                        "created_at": "2026-09-15T22:42:39.408Z",
                        "expires_at": "2026-09-15T22:44:39.349Z"
                      }
                    }
                  },
                  "replayed": {
                    "value": {
                      "consumed": false,
                      "reason": "SESSION_ALREADY_USED",
                      "consumed_at": "2026-09-15T22:43:10.100Z"
                    }
                  },
                  "expired": {
                    "value": {
                      "consumed": false,
                      "reason": "SESSION_EXPIRED",
                      "expires_at": "2026-09-15T22:44:39.349Z"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/merchant-theme-get": {
      "get": {
        "tags": [
          "Surface"
        ],
        "operationId": "merchantThemeGet",
        "summary": "The merchant's theme tokens",
        "description": "Appearance only — no key, no policy, no threshold, no subject data. A publishable\n`pk_*` is accepted because the hosted surface must paint itself before any identity\nexists, and nothing sensitive is reachable here even with a stolen browser key.\n\n22 tokens on the capture surface; the mandate surface consumes the 19 that remain\nafter dropping `logo_icon_url`, `color_success` and `color_danger`.\n\nA merchant with no row — or with any token left NULL — receives the fully populated\n**Sidaxis** theme, and `customized: false` says so. The surface never receives NULL\nand never needs a local fallback. Logos default to `null`; the component is\ndesigned to render without one.\n\n**Read-only by design.** There is no endpoint to write a theme: themes are written\nin the developer console, as the authenticated merchant owner, and only published\nthemes are served. If you are looking for a write API, it does not exist — do not\nbuild against one.\n\n**Hosted mode applies the theme for you.** When the buyer is on a Sidaxis-hosted\nscreen, the screen fetches and applies the theme itself — do not call this endpoint\nin that flow. Call it only in headless/SDK integrations, where your own UI renders\nand you must apply the tokens yourself.\n\nWhite-label is **included** — no per-check cost, no add-on. Branding every hosted\nscreen with the merchant's identity is part of the service.\n\n`cache-control: public, max-age=60`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          },
          {
            "name": "merchant_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "surface",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "capture",
                "mandate"
              ],
              "default": "capture"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The theme.",
            "content": {
              "application/json": {
                "example": {
                  "version": "theme.v1",
                  "merchant_id": "sidaxis-surface-sandbox",
                  "surface": "capture",
                  "customized": false,
                  "tokens": [
                    "brand_name",
                    "mode",
                    "logo_wide_url",
                    "…"
                  ],
                  "theme": {
                    "brand_name": "Sidaxis",
                    "mode": "light",
                    "color_primary": "212 78% 15%",
                    "radius_md_px": 12
                  }
                }
              }
            }
          },
          "400": {
            "description": "`MERCHANT_ID_REQUIRED` · `INVALID_SURFACE`."
          },
          "401": {
            "description": "`INVALID_API_KEY`."
          },
          "405": {
            "description": "`METHOD_NOT_ALLOWED`."
          }
        }
      },
      "post": {
        "tags": [
          "Surface"
        ],
        "operationId": "merchantThemeGetPost",
        "summary": "The merchant's theme tokens (POST form)",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "merchant_id"
                ],
                "properties": {
                  "merchant_id": {
                    "type": "string"
                  },
                  "surface": {
                    "type": "string",
                    "enum": [
                      "capture",
                      "mandate"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The theme."
          }
        }
      }
    },
    "/alias-availability": {
      "post": {
        "tags": [
          "Surface"
        ],
        "operationId": "aliasAvailability",
        "summary": "Is this handle free — and hold it briefly",
        "description": "Real-time availability for a typing surface. A publishable `pk_*` is valid; an\nanonymous caller (no merchant key) may call `check` only.\n\nFormat is engine-owned: `^[a-z][a-z0-9_]{2,29}$` — lowercase, digits and\nunderscore, must start with a letter, no trailing `_`, no `__`. Input is normalised\nbefore validation (`@` dropped, trimmed, lowercased, invalid characters stripped)\nand the normalised alias is echoed back, so the screen shows exactly what would be\nclaimed.\n\nA bad format is **200 with `format_ok: false`** and a named code, so keystroke\nfeedback never looks like an error.\n\nAbout 90 aliases can never be claimed: brand, institutional, technical surfaces and\nambiguity traps. Suggestions are deterministic — no randomness — and every one is\nverified free before it is offered.\n\nA reservation expires on its own; nothing has to run for the alias to come back.\n\nRate limits per minute: `check` 120 with a merchant key, 30 anonymous per IP;\n`reserve` and `release` 10. Responses are `cache-control: no-store`. Debounce\nkeystrokes around 250 ms.\n\n`user_id` is never accepted and never returned here. The identity id is minted by\nthe engine at enrollment; an alias is only a public handle.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/ApiKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "alias"
                ],
                "properties": {
                  "alias": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "check",
                      "reserve",
                      "release"
                    ],
                    "default": "check"
                  },
                  "ttl_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 900,
                    "default": 300
                  },
                  "claim_token": {
                    "type": "string",
                    "description": "Required on release."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Answered.",
            "content": {
              "application/json": {
                "examples": {
                  "free": {
                    "value": {
                      "alias": "silvio",
                      "available": true,
                      "format_ok": true,
                      "suggestions": [],
                      "rules": {
                        "min_length": 3,
                        "max_length": 30,
                        "pattern": "^[a-z][a-z0-9_]{2,29}$"
                      }
                    }
                  },
                  "taken": {
                    "value": {
                      "alias": "silvio",
                      "available": false,
                      "format_ok": true,
                      "suggestions": [
                        "silvio_id",
                        "silvio_official",
                        "the_silvio"
                      ]
                    }
                  },
                  "badFormat": {
                    "value": {
                      "alias": "si",
                      "available": false,
                      "format_ok": false,
                      "code": "ALIAS_TOO_SHORT",
                      "message": "At least 3 characters."
                    }
                  },
                  "reserved": {
                    "value": {
                      "alias": "silvio",
                      "reserved": true,
                      "claim_token": "36402df4…",
                      "expires_at": "2026-09-15T20:44:27.092Z",
                      "ttl_seconds": 300
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`ALIAS_TAKEN` — already held, already an identity, or lost the race."
          },
          "429": {
            "description": "`RATE_LIMITED` with `retry_after_seconds`, `Retry-After` and `X-RateLimit-*`."
          }
        }
      }
    },
    "/get-merchant-receipts-v2": {
      "get": {
        "tags": [
          "Receipts"
        ],
        "operationId": "merchantReceipts",
        "summary": "Receipts for a merchant you own",
        "description": "Auth here is a **first-party Sieve ID JWT** (`Authorization: Bearer`), not an API\nkey: the ownership model is \"the Sieve ID user owns the merchant\". A merchant key\ngets `403 sieve_id_jwt_required`.\n\nBecause the merchant is party to the transaction, these receipts carry the numeric\n`vrc_confidence`, which the public projection never shows.\n",
        "parameters": [
          {
            "name": "merchant_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "receipt_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The receipts.",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "merchant_id": "…",
                  "merchant_name": "…",
                  "total": 2,
                  "receipts": []
                }
              }
            }
          },
          "400": {
            "description": "`missing_required_fields`."
          },
          "401": {
            "description": "`unauthorized`."
          },
          "403": {
            "description": "`caller_not_owner` · `sieve_id_jwt_required`."
          },
          "404": {
            "description": "`unknown_merchant`."
          }
        }
      }
    },
    "/rpc/get_receipt_v2": {
      "post": {
        "tags": [
          "Receipts"
        ],
        "operationId": "verifyReceipt",
        "summary": "Public verification — no API key",
        "description": "A `SECURITY DEFINER` RPC on the Data API, reachable with the publishable anon key.\nNote the different path: `https://api.sidaxis.com/rest/v1/rpc/get_receipt_v2`.\n\nAn unknown hash returns `200 null`, never a 404, so nobody can probe for existence.\n\nThe public projection omits `vrc_confidence` and `biovital_token_hash`, and reports\nproof strength honestly. A receipt written before assurance existed says so\n(`code: not_recorded`) instead of inferring a level after the fact.\n\nHuman-facing page: `/verify/:hash`.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "lookup_key"
                ],
                "properties": {
                  "lookup_key": {
                    "type": "string",
                    "description": "A sieve_hash or a receipt_code."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The redacted receipt, or `null`.",
            "content": {
              "application/json": {
                "example": {
                  "receipt_code": "RCP-7F2C91",
                  "sieve_hash": "0x…",
                  "immutability_seal": "SEAL-…",
                  "assurance": {
                    "code": "face_voice",
                    "i18n_key": "assurance.face_voice",
                    "label": "Face and voice",
                    "description": "A living person was present and spoke, with speech correlated to the pulse read by the camera at the same instant.",
                    "recorded": true,
                    "requires": {
                      "face": true,
                      "voice": true,
                      "document": false
                    }
                  },
                  "mandate": {
                    "id": "mnd_live_b815…",
                    "assurance_level": "face_voice",
                    "granted_to": "agent_shopping_assistant",
                    "agent_display_name": "your travel assistant",
                    "agent_descriptor": "Books flights within your limits"
                  }
                }
              }
            }
          },
          "extra": {
            "liveness_passed": true,
            "vrc_pass": true,
            "biovital_sealed": true
          }
        }
      }
    },
    "/agent-verification-request": {
      "post": {
        "tags": [
          "One Face"
        ],
        "operationId": "agentVerificationRequest",
        "summary": "Mint a One Face card an agent can hand to a person",
        "description": "In production. An agent asks for a verification; the person proves it on her own\ndevice. The agent never touches a camera or a microphone.\n\nAuth is a **first-party Sieve ID JWT** (`Authorization: Bearer`), or the OAuth 2.1\nbearer issued to an MCP client. A merchant API key is refused with\n`USER_TOKEN_REQUIRED`: the card belongs to the merchant the agent represents, and\nonly an owner of that merchant can mint one.\n\nThe response carries a single-use URL on the hosted surface\n(`https://id.sidaxis.com/capture?hs=…`). Default lifetime 300 s, maximum 900 s.\nThis is not a mandate and issues no authority — it is a verification of presence,\nread back with `agent-verification-result`.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "merchant_id"
                ],
                "properties": {
                  "merchant_id": {
                    "type": "string"
                  },
                  "external_ref": {
                    "type": "string",
                    "description": "Your own reference for this verification."
                  },
                  "purpose": {
                    "type": "string",
                    "maxLength": 140,
                    "description": "Shown to the person on the screen."
                  },
                  "ttl_seconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 900,
                    "default": 300
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Card minted.",
            "content": {
              "application/json": {
                "example": {
                  "verification_id": "a87f81d8-…",
                  "url": "https://id.sidaxis.com/capture?hs=hs_live_4ad6…0a17",
                  "merchant_id": "acme-travel",
                  "external_ref": "chk_10231",
                  "expires_at": "2026-09-16T12:05:39.349Z",
                  "ttl_seconds": 300
                }
              }
            }
          },
          "401": {
            "description": "`USER_TOKEN_REQUIRED` — a merchant API key cannot mint a card."
          },
          "403": {
            "description": "`caller_not_owner` — the caller does not own that merchant."
          }
        }
      }
    },
    "/agent-verification-result": {
      "get": {
        "tags": [
          "One Face"
        ],
        "operationId": "agentVerificationResult",
        "summary": "Read the outcome of a One Face card",
        "description": "In production. Polling, not webhooks. Every state is `HTTP 200` with three message\nlayers — `reason`, `developer_message`, `user_message` — never an error.\n\nStates: `pending` (never opened), `opened` (in progress), `verified`, `refused`,\n`expired`. The outcome is derived from what the capture recorded; nothing and nobody\ncan declare `verified` on its own. A verified result carries the assurance level and\nthe receipt.\n",
        "security": [],
        "parameters": [
          {
            "name": "verification_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current state.",
            "content": {
              "application/json": {
                "examples": {
                  "pending": {
                    "value": {
                      "state": "pending",
                      "reason": "verification_not_opened",
                      "developer_message": "The person has not opened the card yet.",
                      "user_message": "Waiting for the person to open the link."
                    }
                  },
                  "verified": {
                    "value": {
                      "state": "verified",
                      "reason": "verified",
                      "developer_message": "Liveness proof recorded at capture time.",
                      "user_message": "Identity verified.",
                      "assurance": {
                        "code": "face_voice",
                        "label": "Face and voice",
                        "recorded": true
                      },
                      "receipt": {
                        "receipt_code": "RCP-7F2C91",
                        "sieve_hash": "0x…"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`USER_TOKEN_REQUIRED`."
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "MCP"
        ],
        "operationId": "mcpServer",
        "summary": "MCP server — nine tools, each on a live endpoint",
        "description": "In production at `https://api.sidaxis.com/functions/v1/mcp` (Streamable HTTP).\nAuthorisation is OAuth 2.1 with dynamic client registration; the person signs in\nand approves the client, and every tool then acts as that user.\n\nNine tools, each mapped one-to-one onto an endpoint in this document:\n\n| Tool | Endpoint |\n|---|---|\n| `mandate_scopes` | `mandate-scopes` |\n| `mandate_check` | `mandate-check` (advisory) |\n| `mandate_consume` | `mandate-consume` (authoritative, `idempotency_key` required) |\n| `mandate_get` | `mandate-get` |\n| `mandate_revoke` | `mandate-revoke` |\n| `mandate_issue` | `mandate-issue` (requires a live `capture_id`) |\n| `receipt_verify` | `rpc/get_receipt_v2` |\n| `identity_verification_request` | `agent-verification-request` |\n| `identity_verification_result` | `agent-verification-result` |\n\nFace and voice capture are deliberately absent, and the absence is the argument: a\ndescriptor is produced on the person's own device by camera and microphone, so no\nagent can produce one. An agent asks; the person proves.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "JSON-RPC response over the MCP Streamable HTTP transport."
          },
          "401": {
            "description": "Missing or invalid OAuth bearer; the resource metadata points at the authorisation server."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-sidaxis-api-key",
        "description": "`sk_live_…` / `sk_test_…` for server calls, `pk_live_…` / `pk_test_…` for browser\ncalls. The legacy header `x-sieve-api-key` is accepted with identical semantics;\nwhen both are present, `x-sidaxis-api-key` wins.\n"
      }
    },
    "parameters": {
      "ApiKey": {
        "name": "x-sidaxis-api-key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Legacy `x-sieve-api-key` accepted, identical semantics."
      },
      "IdempotencyKeyRequired": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 200
        },
        "description": "Mandatory on consume. Without it a network timeout becomes a double charge."
      },
      "IdempotencyKeyOptional": {
        "name": "Idempotency-Key",
        "in": "header",
        "schema": {
          "type": "string",
          "minLength": 8,
          "maxLength": 200
        },
        "description": "Strongly recommended. A replay returns the original body with `Idempotency-Status: replayed`."
      },
      "Timestamp": {
        "name": "x-sidaxis-timestamp",
        "in": "header",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "Unix seconds, ±300 s."
      },
      "Nonce": {
        "name": "x-sidaxis-nonce",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 8
        },
        "description": "Single use."
      },
      "AcceptLanguage": {
        "name": "accept-language",
        "in": "header",
        "schema": {
          "type": "string",
          "enum": [
            "en",
            "pt-BR",
            "es"
          ]
        },
        "description": "Localises `user_message` and scope labels only. Never `reason`."
      }
    },
    "schemas": {
      "FaceTemplate": {
        "type": "object",
        "description": "A descriptor derived on the person's device. Not video, not frames, not audio.\nPayload cap 1 MB. The vector is shredded in-browser after use.\n",
        "required": [
          "vector",
          "quality"
        ],
        "properties": {
          "vector": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 128,
            "maxItems": 128
          },
          "quality": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "BiometricVitals": {
        "type": "object",
        "properties": {
          "bpm": {
            "type": "number"
          },
          "hrv": {
            "type": "number"
          },
          "stress_score": {
            "type": "number"
          },
          "voice_pulse_correlation": {
            "type": "number",
            "description": "Speech correlated with the pulse read by the camera at the same instant. Gate 0.70."
          },
          "liveness_confidence": {
            "type": "number"
          },
          "liveness_proof": {
            "type": "string"
          }
        }
      },
      "EnrollmentCreate": {
        "type": "object",
        "required": [
          "action",
          "merchant_id",
          "sieve_hash",
          "consent",
          "face_template",
          "biometric_vitals"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "ACTION_GENESIS"
            ]
          },
          "merchant_id": {
            "type": "string"
          },
          "sieve_hash": {
            "type": "string"
          },
          "consent": {
            "type": "object",
            "required": [
              "version",
              "locale",
              "accepted_at"
            ],
            "properties": {
              "version": {
                "type": "string"
              },
              "locale": {
                "type": "string"
              },
              "accepted_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "device_signature": {
            "type": "string"
          },
          "client_instance_id": {
            "type": "string"
          },
          "device_label": {
            "type": "string"
          },
          "face_template": {
            "$ref": "#/components/schemas/FaceTemplate"
          },
          "biometric_vitals": {
            "$ref": "#/components/schemas/BiometricVitals"
          }
        }
      },
      "TransactionCreate": {
        "type": "object",
        "required": [
          "action",
          "sieve_hash",
          "user_id",
          "merchant_id"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "ACTION_LOGIN",
              "ACTION_PAYMENT",
              "ACTION_SIGN",
              "ACTION_SIGNUP"
            ]
          },
          "sieve_hash": {
            "type": "string"
          },
          "user_id": {
            "type": "string",
            "format": "uuid",
            "description": "Minted by the engine at enrollment. Never chosen by a surface."
          },
          "merchant_id": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "description": "Minor unit. A float is refused with AMOUNT_INVALID."
          },
          "currency": {
            "type": "string"
          },
          "external_txn_ref": {
            "type": "string"
          },
          "device_signature": {
            "type": "string"
          },
          "client_instance_id": {
            "type": "string"
          },
          "biometric_vitals": {
            "$ref": "#/components/schemas/BiometricVitals"
          },
          "mandate_id": {
            "type": "string",
            "description": "Optional. Present, the mandate stands in for the live capture."
          }
        }
      },
      "MandateCreate": {
        "type": "object",
        "required": [
          "granted_by",
          "granted_to",
          "scopes",
          "ceiling",
          "expires_at",
          "proof"
        ],
        "properties": {
          "granted_by": {
            "type": "string",
            "format": "uuid",
            "description": "The human principal."
          },
          "granted_to": {
            "type": "string",
            "description": "An opaque machine identifier. Never the label a person reads."
          },
          "granted_to_kind": {
            "type": "string",
            "enum": [
              "agent",
              "application",
              "service"
            ]
          },
          "agent_display_name": {
            "type": "string",
            "maxLength": 64,
            "description": "The readable agent name for the screens (\"your travel assistant\"). Frozen with\nthe mandate. Absent stays absent — the screen falls back to the identifier\nrather than inventing a name.\n"
          },
          "agent_descriptor": {
            "type": "string",
            "maxLength": 140,
            "description": "One line saying what the agent does."
          },
          "scopes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scope"
            },
            "description": "Explicit list, matched exactly. No wildcard, no prefix."
          },
          "constraints": {
            "type": "object",
            "description": "Mandatory for `task` — at least one non-empty list.",
            "properties": {
              "merchant_allowlist": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "category_allowlist": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "country_allowlist": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "ceiling": {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "amount",
                  "count",
                  "both",
                  "none"
                ]
              },
              "amount": {
                "type": "integer",
                "description": "Minor unit. 4000 is forty dollars. Never a float. Only on `purchase` and `payment`."
              },
              "currency": {
                "type": "string",
                "description": "Required when the ceiling is amount-based."
              },
              "count": {
                "type": "integer",
                "description": "Required when the ceiling is count-based."
              },
              "period": {
                "type": "string",
                "enum": [
                  "lifetime",
                  "daily",
                  "weekly",
                  "monthly"
                ],
                "description": "The bucket is computed server-side. A client-sent period anchor is ignored."
              }
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Mandatory."
          },
          "step_up_of": {
            "type": "string",
            "description": "The mandate this one supersedes. The parent is never consumed, changed or reset."
          },
          "proof": {
            "type": "object",
            "required": [
              "capture_id"
            ],
            "properties": {
              "capture_id": {
                "type": "string",
                "description": "A capture no older than five minutes, with liveness passed and no coercion signal."
              }
            }
          }
        }
      },
      "Scope": {
        "type": "string",
        "enum": [
          "login",
          "purchase",
          "payment",
          "task",
          "signature",
          "data_share",
          "id_proof",
          "access"
        ],
        "description": "The closed v2 vocabulary, matched exactly. Recurrence lives in `ceiling.period` and\nnowhere else. Only `purchase` and `payment` accept an amount ceiling; `task`\nrequires constraints.\n"
      },
      "Mandate": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MandateCreate"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "examples": [
                  "mnd_live_b8158ce8513523e91f74c6bc5a0a13e8"
                ]
              },
              "merchant_id": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "active",
                  "consumed",
                  "exhausted",
                  "expired",
                  "revoked"
                ]
              },
              "stored_status": {
                "type": "string",
                "description": "The untouched row, when it differs from the effective status."
              },
              "assurance_level": {
                "$ref": "#/components/schemas/AssuranceLevel"
              },
              "issuance_proof_id": {
                "type": "string"
              },
              "device_key_id": {
                "type": "string"
              },
              "issued_at": {
                "type": "string",
                "format": "date-time"
              },
              "remaining": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "count": {
                    "type": "integer"
                  }
                }
              },
              "period_resets_at": {
                "type": "string",
                "format": "date-time"
              },
              "scope_labels": {
                "type": "array",
                "description": "Ready in the caller's locale, so the consent screen does not write its own.",
                "items": {
                  "type": "object",
                  "properties": {
                    "scope": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "description": "{ type: string }",
                    "limits": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "consumptions": {
                "type": "array",
                "description": "The last 100, on a one-mandate read.",
                "items": {
                  "type": "object"
                }
              }
            }
          }
        ]
      },
      "AssuranceLevel": {
        "type": "string",
        "enum": [
          "face",
          "face_voice",
          "face_voice_doc"
        ],
        "description": "Derived at the instant of the act and frozen on the receipt. Never sent by the\nclient. There is no identity-level assurance column and there must never be one —\nthe level belongs to the act.\n"
      },
      "MandateActionRequest": {
        "type": "object",
        "required": [
          "mandate_id",
          "scope"
        ],
        "properties": {
          "mandate_id": {
            "type": "string"
          },
          "scope": {
            "$ref": "#/components/schemas/Scope"
          },
          "amount": {
            "type": "integer",
            "description": "Minor unit."
          },
          "currency": {
            "type": "string"
          },
          "action_ref": {
            "type": "string"
          },
          "context": {
            "type": "object",
            "properties": {
              "merchant": {
                "type": "string"
              },
              "category": {
                "type": "string"
              },
              "country": {
                "type": "string"
              }
            }
          }
        }
      },
      "MandateAnswer": {
        "type": "object",
        "required": [
          "allowed"
        ],
        "description": "Always HTTP 200. Three message layers: `reason` is machine-stable and never\ntranslated; `developer_message` is always English, for logs; `user_message` is\nlocalised by `Accept-Language` and is deliberately absent on integration errors.\n",
        "properties": {
          "allowed": {
            "type": "boolean"
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "developer_message": {
            "type": "string"
          },
          "user_message": {
            "type": "string",
            "description": "Absent on `not_found`, `rate_limited` and `idempotency_key_required`."
          },
          "step_up_available": {
            "type": "boolean",
            "description": "Present on `ceiling_exceeded` and `scope_mismatch`."
          },
          "consumption_id": {
            "type": "string"
          },
          "idempotent_replay": {
            "type": "boolean"
          },
          "at_limit": {
            "type": "boolean"
          },
          "remaining": {
            "type": "object"
          },
          "period_resets_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Reason": {
        "type": "string",
        "enum": [
          "ceiling_exceeded",
          "scope_mismatch",
          "mandate_expired",
          "mandate_revoked",
          "constraint_violated",
          "liveness_required",
          "not_found",
          "rate_limited",
          "idempotency_key_required"
        ],
        "description": "Stable forever, never translated, never in a locale file. Branch on this, never on\nthe HTTP status. `liveness_required` is a refusal in a 200 body — not an HTTP error.\n"
      },
      "DocumentRequired": {
        "type": "object",
        "description": "`403` from `process-biovital-transaction` (payment, signature) and from\n`mandate-issue` on a money scope. Once per identity, not per operation: with a\nvalid record the operation never asks.\n\n`liveness_reusable: false` is not decoration — the retry needs a fresh capture. The\ndocument step does not extend the liveness window.\n",
        "properties": {
          "success": {
            "type": "boolean",
            "examples": [
              false
            ]
          },
          "code": {
            "type": "string",
            "examples": [
              "DOCUMENT_REQUIRED"
            ]
          },
          "required_assurance_level": {
            "type": "string",
            "examples": [
              "face_voice_doc"
            ]
          },
          "current_assurance_level": {
            "type": "string",
            "examples": [
              "face_voice"
            ]
          },
          "document_status": {
            "type": "string",
            "enum": [
              "missing",
              "expired"
            ]
          },
          "document_valid_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "liveness_reusable": {
            "type": "boolean",
            "examples": [
              false
            ]
          },
          "rule": {
            "type": "string",
            "examples": [
              "once_per_identity"
            ]
          }
        }
      },
      "HostedSessionCreate": {
        "type": "object",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "authorize",
              "checkin",
              "sign",
              "verify",
              "enroll"
            ]
          },
          "external_txn_ref": {
            "type": "string",
            "maxLength": 128,
            "description": "Required for `authorize`. Charset `[\\w.:@/-]`."
          },
          "amount_cents": {
            "type": "integer",
            "description": "Required for `authorize`. Minor unit."
          },
          "currency": {
            "type": "string",
            "description": "ISO-4217. Defaults to BRL when an amount is present."
          },
          "brand": {
            "type": "string",
            "maxLength": 40,
            "description": "Display label, sanitized. Full theming comes from merchant-theme-get."
          },
          "ttl_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 900,
            "default": 300
          },
          "metadata": {
            "type": "object",
            "description": "Display and passthrough only, max 2 KB."
          },
          "merchant_id": {
            "type": "string",
            "description": "If sent, must equal the key's merchant."
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "description": "Written to an outbox in the **same transaction** as the consumption and delivered by\n`mandate-outbox-worker`, which runs **every minute**. A webhook inside the\ntransaction would hold the ceiling lock hostage to a slow client; a send before\ncommit would announce something that never happened.\n\nAt-least-once. Deduplicate on the event id.\n\nSignature over the exact bytes sent:\n\n```\nsignature = \"sha256=\" + HMAC_SHA256(webhook_secret, `${unix_seconds}.${raw_body}`)\n```\n\nHeaders: `x-sidaxis-signature`, `x-sidaxis-timestamp`, `x-sidaxis-event-id`, plus\nthe legacy `x-sieve-*` triple with identical values. Verify against the raw body —\nre-serialising the JSON breaks the check.\n\nBackoff: 30 s → 2 m → 10 m → 1 h → 6 h, then parked.\n",
        "required": [
          "event_id",
          "type",
          "created_at",
          "data"
        ],
        "properties": {
          "event_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "mandate.granted",
              "mandate.consumed",
              "mandate.at_limit",
              "mandate.revoked",
              "mandate.expired"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object"
          }
        }
      },
      "TransportError": {
        "type": "object",
        "description": "A real HTTP error — malformed request or authentication failure. Not a refusal.",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "`INVALID_REQUEST` — malformed body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TransportError"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`API_KEY_REQUIRED` · `INVALID_API_KEY` · `AUTH_REQUIRED` (a `pk_*` on a server endpoint).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TransportError"
            }
          }
        }
      },
      "RateLimited": {
        "description": "`RATE_LIMITED`, with `Retry-After`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TransportError"
            }
          }
        }
      }
    }
  }
}
