{
  "openapi": "3.1.0",
  "info": {
    "title": "OBVELO gateway API",
    "version": "1",
    "description": "Masks personal data before it reaches a language model and restores it in the answer. The data plane stores nothing from request content. Every route except `GET /health` takes the gateway key. Send it as `x-obvelo-key` (recommended), `x-api-key`, or `authorization: Bearer`. On the model-proxy paths `authorization` belongs to the provider, so there the key goes in `x-obvelo-key` or `x-api-key`.",
    "contact": {
      "email": "office@obvelo.com"
    }
  },
  "servers": [
    {
      "url": "https://api.obvelo.com"
    }
  ],
  "security": [
    {
      "obveloKey": []
    },
    {
      "apiKey": []
    },
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "Masking"
    },
    {
      "name": "Account"
    },
    {
      "name": "Model proxy"
    },
    {
      "name": "Operations"
    },
    {
      "name": "Extension",
      "description": "The browser extension's content-free activity trail."
    }
  ],
  "paths": {
    "/v1/mask": {
      "post": {
        "summary": "Mask text or a structure",
        "description": "Replaces personal data with tokens such as `[Person AG26KCY5GDJM76NS73FA6RKFNP-1]` and returns the map to restore them. Billed per character. Nothing from the request is stored.",
        "tags": [
          "Masking"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/MaskTextRequest"
                  },
                  {
                    "$ref": "#/components/schemas/MaskObjectRequest"
                  }
                ]
              },
              "examples": {
                "text": {
                  "value": {
                    "text": "Write to Jan Przykladowy, jan@acme.example."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Masked. Check `safetyNet` before sending the text to a model.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/MaskTextResponse"
                    },
                    {
                      "$ref": "#/components/schemas/MaskObjectResponse"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "The request carries a file this gateway does not mask, and the account's attachment policy is `block`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Masking failed. Nothing was returned in clear.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/inspect": {
      "post": {
        "summary": "See what would be masked, and why",
        "description": "Returns each detection with its position, label, rule and confidence. For previews and tuning. Billed like `/v1/mask`.",
        "tags": [
          "Masking"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The detections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InspectResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "500": {
            "description": "Inspection failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/restore": {
      "post": {
        "summary": "Put the original values back",
        "description": "Replaces tokens with values from the map you send. Not billed. The SDKs do this locally, so the map never leaves your process.",
        "tags": [
          "Masking"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RestoreRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RestoreResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          }
        }
      }
    },
    "/v1/whoami": {
      "get": {
        "summary": "What this key is worth",
        "description": "Plan, quota, limits, settings, and what there is to narrow. Answers even when the quota is spent.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "The key's account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Whoami"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          }
        }
      },
      "post": {
        "summary": "What this key is worth",
        "description": "Same as `GET`.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "The key's account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Whoami"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          }
        }
      }
    },
    "/v1/extension/events": {
      "post": {
        "summary": "Report the browser extension's activity (content-free)",
        "description": "Sent by the OBVELO browser extension, with the tenant key it already uses, for the administrator's audit trail: a heartbeat at browser start and at most every six hours, pausing and resuming, a chat site added or removed (hostname only), a file attached unmasked (file TYPE only, from a closed list), administrator sign-in and failed sign-in, settings changed (setting NAMES only), a dictionary loaded (entry COUNT only). Each event carries a random installation id and no user identity. The gateway validates every event against a fixed schema and refuses the whole batch when one event carries anything else — no event may carry content. Only for accounts with the browser add-on. Not billed.",
        "tags": [
          "Extension"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExtensionEvents"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtensionEventsAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Is this instance ready",
        "description": "No key. For load balancers and monitoring.",
        "tags": [
          "Operations"
        ],
        "responses": {
          "200": {
            "description": "Instance state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/chat/completions": {
      "post": {
        "summary": "OpenAI Chat Completions",
        "description": "The OpenAI Chat Completions API, with personal data masked on the way out and restored on the way back, streamed or not. Point the provider's own client at this address; its key travels in the request and is never stored. Settings come from the key, not from the body. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The provider's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBody"
                }
              }
            },
            "headers": {
              "x-pii-attachments": {
                "description": "What happened to files in the request: `policy=…; inline=N; references=N; masked=all|some|none; maskedCount=N`, then `reasons=…` and, only when an Office file hid something, `removed=N; imagesReplaced=N; imagesKept=N; embeddedMasked=N; embeddedRemoved=N` summed over every file. Numbers only.",
                "schema": {
                  "type": "string"
                }
              },
              "x-pii-dictionary": {
                "description": "How much of the dictionary was used.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "Data was left after masking, or a file this key's policy refuses (under `attachmentPolicy` `mask`: any file that could not be masked, including a provider file reference). Nothing was sent to the provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "This instance does not proxy this protocol.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      }
    },
    "/v1/responses": {
      "post": {
        "summary": "OpenAI Responses",
        "description": "The OpenAI Responses API, with personal data masked on the way out and restored on the way back, streamed or not. Point the provider's own client at this address; its key travels in the request and is never stored. Settings come from the key, not from the body. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The provider's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBody"
                }
              }
            },
            "headers": {
              "x-pii-attachments": {
                "description": "What happened to files in the request: `policy=…; inline=N; references=N; masked=all|some|none; maskedCount=N`, then `reasons=…` and, only when an Office file hid something, `removed=N; imagesReplaced=N; imagesKept=N; embeddedMasked=N; embeddedRemoved=N` summed over every file. Numbers only.",
                "schema": {
                  "type": "string"
                }
              },
              "x-pii-dictionary": {
                "description": "How much of the dictionary was used.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "Data was left after masking, or a file this key's policy refuses (under `attachmentPolicy` `mask`: any file that could not be masked, including a provider file reference). Nothing was sent to the provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "This instance does not proxy this protocol.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      }
    },
    "/v1/messages": {
      "post": {
        "summary": "Anthropic Messages",
        "description": "The Anthropic Messages API, with personal data masked on the way out and restored on the way back, streamed or not. Point the provider's own client at this address; its key travels in the request and is never stored. Settings come from the key, not from the body. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The provider's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBody"
                }
              }
            },
            "headers": {
              "x-pii-attachments": {
                "description": "What happened to files in the request: `policy=…; inline=N; references=N; masked=all|some|none; maskedCount=N`, then `reasons=…` and, only when an Office file hid something, `removed=N; imagesReplaced=N; imagesKept=N; embeddedMasked=N; embeddedRemoved=N` summed over every file. Numbers only.",
                "schema": {
                  "type": "string"
                }
              },
              "x-pii-dictionary": {
                "description": "How much of the dictionary was used.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "Data was left after masking, or a file this key's policy refuses (under `attachmentPolicy` `mask`: any file that could not be masked, including a provider file reference). Nothing was sent to the provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "This instance does not proxy this protocol.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      }
    },
    "/v1beta/models/{model}:generateContent": {
      "post": {
        "summary": "Gemini generateContent",
        "description": "The Gemini API, with personal data masked on the way out and restored on the way back, streamed or not. Point the provider's own client at this address; its key travels in the request and is never stored. Settings come from the key, not from the body. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The provider's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBody"
                }
              }
            },
            "headers": {
              "x-pii-attachments": {
                "description": "What happened to files in the request: `policy=…; inline=N; references=N; masked=all|some|none; maskedCount=N`, then `reasons=…` and, only when an Office file hid something, `removed=N; imagesReplaced=N; imagesKept=N; embeddedMasked=N; embeddedRemoved=N` summed over every file. Numbers only.",
                "schema": {
                  "type": "string"
                }
              },
              "x-pii-dictionary": {
                "description": "How much of the dictionary was used.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "Data was left after masking, or a file this key's policy refuses (under `attachmentPolicy` `mask`: any file that could not be masked, including a provider file reference). Nothing was sent to the provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "This instance does not proxy this protocol.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      },
      "parameters": [
        {
          "name": "model",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "gemini-2.0-flash"
        }
      ]
    },
    "/v1beta/models/{model}:streamGenerateContent": {
      "post": {
        "summary": "Gemini streamGenerateContent",
        "description": "The Gemini API, with personal data masked on the way out and restored on the way back, streamed or not. Point the provider's own client at this address; its key travels in the request and is never stored. Settings come from the key, not from the body. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The provider's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBody"
                }
              }
            },
            "headers": {
              "x-pii-attachments": {
                "description": "What happened to files in the request: `policy=…; inline=N; references=N; masked=all|some|none; maskedCount=N`, then `reasons=…` and, only when an Office file hid something, `removed=N; imagesReplaced=N; imagesKept=N; embeddedMasked=N; embeddedRemoved=N` summed over every file. Numbers only.",
                "schema": {
                  "type": "string"
                }
              },
              "x-pii-dictionary": {
                "description": "How much of the dictionary was used.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "Data was left after masking, or a file this key's policy refuses (under `attachmentPolicy` `mask`: any file that could not be masked, including a provider file reference). Nothing was sent to the provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "This instance does not proxy this protocol.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      },
      "parameters": [
        {
          "name": "model",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "example": "gemini-2.0-flash"
        }
      ]
    },
    "/mistral/v1/chat/completions": {
      "post": {
        "summary": "Mistral Chat Completions",
        "description": "The Mistral chat API, with personal data masked on the way out and restored on the way back, streamed or not. Point the provider's own client at this address; its key travels in the request and is never stored. Settings come from the key, not from the body. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The provider's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProviderBody"
                }
              }
            },
            "headers": {
              "x-pii-attachments": {
                "description": "What happened to files in the request: `policy=…; inline=N; references=N; masked=all|some|none; maskedCount=N`, then `reasons=…` and, only when an Office file hid something, `removed=N; imagesReplaced=N; imagesKept=N; embeddedMasked=N; embeddedRemoved=N` summed over every file. Numbers only.",
                "schema": {
                  "type": "string"
                }
              },
              "x-pii-dictionary": {
                "description": "How much of the dictionary was used.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "422": {
            "description": "Data was left after masking, or a file this key's policy refuses (under `attachmentPolicy` `mask`: any file that could not be masked, including a provider file reference). Nothing was sent to the provider.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "This instance does not proxy this protocol.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      },
      "x-edge-route": "Routed by the edge of api.obvelo.com, which removes the /mistral prefix; the gateway itself sees /v1/chat/completions."
    },
    "/mcp": {
      "post": {
        "summary": "MCP tool gateway",
        "description": "JSON-RPC for a Model Context Protocol server behind the gateway: tool arguments and results are masked and restored. Only on an instance set up for it; elsewhere 501. When the model provider answers with an error, the gateway passes its status, its error body and the headers its client library retries by through unchanged, marked `x-obvelo-upstream: provider`, so the provider's own library raises its usual typed error; the gateway's own refusals carry no such mark.",
        "tags": [
          "Model proxy"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProviderBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The MCP server's answer, restored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ProxyBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ProxyUnauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/ProxyForbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/ProxyRateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "501": {
            "description": "This instance does not speak MCP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The gateway could not reach the model provider, or the provider's answer is a status it cannot pass on (a redirect, or a success with no body). It is the gateway's own: it carries no `x-obvelo-upstream`. A provider that itself answers 502 is relayed as an error of the provider's, with the mark.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "4XX": {
            "$ref": "#/components/responses/ProviderError"
          },
          "5XX": {
            "$ref": "#/components/responses/ProviderError"
          }
        }
      }
    },
    "/v1/extension/content": {
      "post": {
        "summary": "Report a file that went to a chat site unmasked (customer-operated gateways only)",
        "description": "Sent by the browser add-on, only to a gateway whose `/v1/whoami` declares `contentLog.enabled`: the name, type and bytes of a file the person attached unmasked, for the customer's own content log. Answers 404 on a gateway that does not record content (our hosted service never does). Not billed.",
        "tags": [
          "Extension"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContentEvents"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContentEventsAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          },
          "404": {
            "description": "This gateway does not record content.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/addon-policy": {
      "get": {
        "summary": "What the browser add-on is locked to",
        "description": "The add-on policy the account's administrator saved (hosted: in the shop's panel; customer-operated: on the gateway's own admin page), with the jurisdictions and groups the account masks under. Answers even when the quota is spent. `configured: false` means nobody saved one: each browser keeps its own settings. Carries switches and hostnames only, never content.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "The policy in force.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddonPolicyAnswer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/SnapshotStale"
          }
        },
        "parameters": [
          {
            "name": "x-obvelo-installation",
            "in": "header",
            "required": false,
            "description": "The add-on's installation id (26 characters). When the administrator gave that browser settings of its own, they are laid over the policy in the answer; otherwise the header changes nothing.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"
            }
          }
        ]
      }
    },
    "/v1/addon-dictionary": {
      "get": {
        "summary": "The organisation's central dictionary (customer-operated gateways)",
        "description": "The dictionary an administrator uploaded on the gateway's /admin/addon-policy page, served to the add-ons that use this gateway. Never leaves the customer's machine. 404 where none was uploaded, and always on the hosted service, which holds no content.",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "The dictionary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "revision": {
                      "type": "string"
                    },
                    "entries": {
                      "type": "integer"
                    },
                    "dictionary": {
                      "type": "object",
                      "properties": {
                        "people": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "organizations": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "custom": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "value": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "value"
                            ]
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "revision",
                    "entries",
                    "dictionary"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No central dictionary."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "obveloKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-obvelo-key",
        "description": "Recommended. Works on every path."
      },
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "On the Anthropic path, send the gateway key in `x-obvelo-key` instead: there `x-api-key` carries the provider's key."
      },
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Not on the model-proxy paths, where `authorization` belongs to the provider."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong, in one sentence. Never echoes request content."
          },
          "hint": {
            "type": "string",
            "description": "What to do about it. Always a fixed sentence, never built from the request."
          },
          "detail": {
            "type": "string",
            "description": "Extra detail on some refusals (an invalid custom pattern, the proxy's safety net)."
          },
          "attachments": {
            "type": "object",
            "description": "On a refusal over files: the policy and counts only, never a file name, id or byte. `reasons` come from a fixed list."
          },
          "reason": {
            "type": "string",
            "description": "Why an account is refused: `unpaid`, `trial_expired`, `cancelled`, `abuse`, `closed`."
          },
          "accountUrl": {
            "type": "string",
            "format": "uri",
            "description": "Where the account owner can act on the refusal. Only on an instance connected to the shop."
          },
          "verify": {
            "type": "string",
            "description": "On 401: how to check what a key is worth."
          },
          "maxValues": {
            "type": "integer"
          },
          "max": {
            "type": "integer"
          },
          "maxKB": {
            "type": "integer"
          },
          "received": {
            "type": "integer"
          },
          "suggestedParts": {
            "type": "integer"
          },
          "maxAttachmentBytes": {
            "type": "integer",
            "description": "413 for a file over the per-file limit: that limit, in bytes, decoded."
          },
          "maxCharacters": {
            "type": "integer",
            "description": "413 for a file whose TEXT is over the key's per-request limit: that limit, in characters."
          },
          "retryAfterSeconds": {
            "type": "integer",
            "description": "503 while the instance works on other large files: when to send again."
          },
          "requestsPerMinute": {
            "type": "integer"
          },
          "plan": {
            "type": "string"
          },
          "monthlyCharacterLimit": {
            "type": "integer"
          },
          "charactersUsed": {
            "type": "integer"
          },
          "tools": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "dialect": {
            "type": "string"
          },
          "status": {
            "type": "integer",
            "description": "On a 502 for a status the gateway cannot relay (a redirect, or a success with no body): the status the provider answered."
          },
          "code": {
            "type": "string",
            "description": "A machine-readable name for the refusal where one exists. `engine-budget-exceeded`: the 503 for a document whose masking ran past the operator's time budget. `name-model-unavailable`: the 503 for a request in which a call to the name model failed (it timed out, the connection dropped, the sidecar was down or answered something unreadable) — nothing was masked, forwarded or billed, and no file is handed back; the `hint` names the remedy. `text-too-long-for-name-model`: the 413 for a request in which ONE text is too long for the name model to read in time (above about 50,000 characters) — refused before the model is asked, nothing masked, forwarded or billed; the `hint` names the remedy (send it in smaller parts, or the account's administrator enables splitting long texts)."
          },
          "budgetMs": {
            "type": "integer",
            "description": "On `engine-budget-exceeded`: the operator's time budget for one engine call, in milliseconds (`ENGINE_TIME_BUDGET_MS`)."
          }
        },
        "required": [
          "error"
        ]
      },
      "Map": {
        "type": "object",
        "additionalProperties": {
          "type": "string"
        },
        "description": "Token → original value. Keep it on your side for the length of the exchange; do not log it. Send it back with the next request of the same conversation so a person keeps one token."
      },
      "DictionaryEntry": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "description": "Letters only, at most 15 characters. Default `Data`."
          }
        },
        "required": [
          "value"
        ]
      },
      "Dictionary": {
        "type": "object",
        "properties": {
          "people": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of people involved — masked as `Person`."
          },
          "organizations": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of organisations — masked as `Company`."
          },
          "custom": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DictionaryEntry"
            },
            "description": "Any other value, with the label to mask it under."
          },
          "never": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Values never to mask."
          }
        },
        "description": "Travels with the request and is not stored. At most 20 000 entries in total."
      },
      "CustomPattern": {
        "type": "object",
        "properties": {
          "pattern": {
            "type": "string",
            "maxLength": 300
          },
          "label": {
            "type": "string"
          },
          "confidence": {
            "type": "number"
          }
        },
        "required": [
          "pattern"
        ],
        "description": "Needs a plan with custom patterns. At most 32; no backreferences, no nested unbounded quantifiers."
      },
      "Options": {
        "type": "object",
        "properties": {
          "countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ISO 3166-1 alpha-2 codes. Intersected with the account's own list — it can narrow, never widen."
          },
          "disabledCategories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Categories to leave unmasked, added to the account's."
          },
          "disabledLabels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "disabledPatterns": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "profile": {
            "type": "string",
            "enum": [
              "default",
              "extended"
            ],
            "description": "`extended` also masks city names, age and nationality."
          },
          "inflection": {
            "type": "string",
            "enum": [
              "off",
              "tag"
            ],
            "description": "Left out, the account's setting applies, and an account that never chose has `tag` in all of `catalogue.inflectionLanguages` (the default since 22.10.2026). `tag` adds a tag after the bar of a token, in words, wherever the engine KNOWS what the form is: `[Person NS-1|dative]`, `[Person NS-1|diminutive]` for a nickname, `[Person NS-1|diminutive, dative]` for both, `[Person NS-1|dative 2]` for a second, different form that reads the same. Case names are nominative, genitive, dative, accusative, instrumental, locative, vocative, ablative. A case is named only when the analyzer is certain."
          },
          "inflectionLanguage": {
            "type": "string",
            "description": "The language of THIS request, for the whole text. Chosen, never guessed. Left out, the account's languages apply (`inflectionLanguages`). The list the instance supports is in `/v1/whoami` under `catalogue.inflectionLanguages`."
          },
          "inflectionLanguages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Replaces, for this request, the languages the account's administrator chose in the panel. Each paragraph is read in the language it is written in, and a paragraph in a language that is not in the list gets no case tags. Codes without an analyzer are ignored; a list of only such codes behaves like `inflection: off`."
          },
          "coreference": {
            "type": "boolean"
          },
          "inferLanguage": {
            "type": "boolean"
          },
          "neverMaskValues": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 200,
            "description": "Exact values to leave in clear for this request."
          },
          "alwaysMaskValues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DictionaryEntry"
            },
            "maxItems": 200,
            "description": "Exact values to mask for this request."
          },
          "attachmentPolicy": {
            "type": "string",
            "enum": [
              "pass",
              "flag",
              "mask",
              "block"
            ],
            "description": "What to do about files in this request, loosest to strictest. The request is honoured only when it is STRICTER than the account's own file policy (`flag` by default, which reports files and does not mask them): asking for `mask` makes the gateway take a file apart, mask it and put it back; the account's `block` and `mask` are never loosened by a request, and the answer's `attachments.policy` says which policy ran. Any other value is refused with 400. Read by `/v1/mask` and `/v1/inspect`; the model proxy and the MCP tool gateway follow the account's policy only."
          },
          "nameFinderMode": {
            "type": "string",
            "enum": [
              "always",
              "escalation",
              "off"
            ],
            "description": "When the name model is asked. Default `always`: every text. `escalation` asks only where the rules found nobody or left a clue, so a single name only the model knows can stay in the clear once the rules have found any person. `off` is rules only."
          },
          "level": {
            "type": "string",
            "enum": [
              "standard",
              "selective"
            ],
            "description": "`disabled` is refused with 400: a request may narrow masking, never switch it off."
          }
        },
        "description": "Everything here can only narrow what the account allows."
      },
      "MaskTextRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "map": {
            "$ref": "#/components/schemas/Map"
          },
          "dictionary": {
            "$ref": "#/components/schemas/Dictionary"
          },
          "patterns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomPattern"
            }
          },
          "options": {
            "$ref": "#/components/schemas/Options"
          },
          "nameTicket": {
            "type": "string",
            "description": "Echo the `nameTicket` of the previous answer, when the instance issues them."
          }
        },
        "required": [
          "text"
        ]
      },
      "MaskObjectRequest": {
        "type": "object",
        "properties": {
          "object": {
            "description": "Any JSON. Structural keys — ids, cursors, thread ids — are never masked. Under the attachment policy `mask`, a file's name beside its bytes (`filename` or `name`) is masked with the file, in the same token state: its words are read across `_`, `-` and `.`, a word of a person the request masked is masked in the name too, and its tokens are written without a tag (`|` is not allowed in a file name). A name with nothing to mask comes back unchanged."
          },
          "map": {
            "$ref": "#/components/schemas/Map"
          },
          "dictionary": {
            "$ref": "#/components/schemas/Dictionary"
          },
          "patterns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomPattern"
            }
          },
          "options": {
            "$ref": "#/components/schemas/Options"
          },
          "attachmentPreview": {
            "type": "boolean",
            "description": "Ask for each masked file's text as the answer's file holds it, with the tokens by position (`attachments.items[].preview`). Read out of the returned bytes, so it shows exactly what the receiver gets; it costs no engine call and bills nothing beyond the file itself. Used by the browser add-on to let a person review a file before it is attached."
          },
          "fileReceipts": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Receipts from an earlier answer (`attachments.items[].receipt`). When every inline file of this request is covered by a receipt this gateway issued for that exact file, to this key's account, and not yet expired (10 minutes), the files are not billed again: a file is billed once, however many times it is re-read while its masks are edited. Text sent beside the files is billed as usual. Nothing is stored: the receipt is an HMAC over the account, the file's digest and the expiry."
          }
        },
        "required": [
          "object"
        ]
      },
      "SafetyNetItem": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string"
          },
          "offset": {
            "type": "integer"
          },
          "length": {
            "type": "integer"
          },
          "text": {
            "type": "string",
            "description": "The value itself. Do not log it."
          }
        },
        "required": [
          "kind",
          "offset",
          "length"
        ]
      },
      "SafetyNet": {
        "type": "object",
        "properties": {
          "summary": {
            "type": "string",
            "description": "Kinds and counts, e.g. `iban x1`."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SafetyNetItem"
            }
          }
        },
        "required": [
          "summary",
          "items"
        ],
        "description": "Present ONLY when unambiguous data (an email, an IBAN, a national ID, a card) was still in the output. The status stays 200: do not send such a result to a model."
      },
      "Language": {
        "type": "object",
        "properties": {
          "resolved": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "description": "Where the language came from: `declared` (the request), `tenant` (a single language in the account), `account` (the account's languages, read paragraph by paragraph), `inferred`, or `none`."
          },
          "accountLanguages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present when `source` is `account`: the languages whose analyzers may be applied, each only to the paragraphs written in it."
          },
          "confidence": {
            "type": "number"
          },
          "runnerUp": {
            "type": "string"
          },
          "abstained": {
            "type": "boolean"
          },
          "appliedTo": {
            "type": "object",
            "properties": {
              "inflection": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "modelHint": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "withheld": {
            "type": "object"
          }
        }
      },
      "DictionaryReport": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "truncated": {
            "type": "boolean"
          },
          "dropped": {
            "type": "integer"
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "entries",
          "limit",
          "truncated",
          "dropped"
        ]
      },
      "AttachmentItem": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "mediaType": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          },
          "masked": {
            "type": "boolean"
          },
          "format": {
            "type": "string",
            "description": "What the file turned out to be, recognised by content: an Office container (`docx`, `xlsx`, `pptx`), an old binary Office file (`doc`, `xls`, `ppt`), an Outlook message (`msg`), `rtf`, an e-mail message (`eml`), `html`, `xml`, `text`, `pdf`, or `image` (a scan or photo read by the `ocr` add-on)."
          },
          "convertedTo": {
            "type": "string",
            "enum": [
              "text",
              "docx",
              "xlsx",
              "pptx",
              "eml"
            ],
            "description": "The file came back as a different kind, with its name and `media_type` changed in the payload to match: a PDF with a text layer — or, with the `ocr` add-on, a scan, a photo or a PDF with no text layer — returns as its masked text (`text`: `media_type` `text/plain`, `.txt` added to its name); an old binary Office file returns as its modern format (`docx`, `xlsx`, `pptx`: `.doc` becomes `.docx` and so on) where the instance runs the converter; an Outlook `.msg` returns as a masked e-mail message (`eml`: `message/rfc822`, `.msg` becomes `.eml`)."
          },
          "reason": {
            "type": "string",
            "description": "Why the file was not masked. `policy-flag`, `policy-pass` and `policy-block` mean nothing tried: the file policy in force does not mask files (the item carries a `hint` naming the remedy). `ocr-refused` means OCR was asked and could not read it; `ocrReason` says why. `conversion-unavailable` means an old binary Office file (.doc, .xls, .ppt) and no converter on this instance; `conversion-refused` means the converter was asked and did not convert (`convertReason` says why) — either way the file was not sent on, and the item carries a `hint`. `encrypted` means a password-protected file."
          },
          "ocrReason": {
            "type": "string",
            "enum": [
              "too-many-pages",
              "too-large",
              "timeout",
              "unreadable",
              "unavailable",
              "quota-exhausted"
            ],
            "description": "Why OCR did not read the file — from this fixed list only. `quota-exhausted`: the plan's monthly OCR page cap is spent (the item carries a `hint`); `unavailable`: this instance has no OCR sidecar."
          },
          "convertReason": {
            "type": "string",
            "enum": [
              "too-large",
              "timeout",
              "unreadable",
              "unavailable"
            ],
            "description": "Why the converter did not convert an old binary Office file — from this fixed list only."
          },
          "hint": {
            "type": "string",
            "description": "The remedy, when there is one a customer can act on: present when `ocrReason` is `quota-exhausted`, when `reason` is `policy-flag`, `policy-pass` or `policy-block`, and when `reason` is `conversion-unavailable` or `conversion-refused`. A fixed sentence — it never carries anything from the request."
          },
          "ocrPages": {
            "type": "integer",
            "description": "Pages OCR read for this file — what counts against the monthly page cap. Not what the file costs: the text it held is billed in characters. For an Office file: pictures inside it that OCR read, one page each."
          },
          "partsRemoved": {
            "type": "integer",
            "description": "Office files only. Parts taken out of the container because they cannot be masked: thumbnail, macro project, signatures, unreadable embedded objects, printer settings and parts on no list. A count — a part name can be content, so none is ever named."
          },
          "unknownPartsRemoved": {
            "type": "integer",
            "description": "Office files only. Of `partsRemoved`, the parts that were on no list at all."
          },
          "imagesReplaced": {
            "type": "integer",
            "description": "Office files only. Pictures inside the file replaced by a neutral placeholder of the same format (every raster picture without the `ocr` add-on; with it, those whose text held something masked, and every metafile)."
          },
          "imagesKept": {
            "type": "integer",
            "description": "Office files only. Pictures the `ocr` add-on read and found nothing in, left as they were."
          },
          "embeddedMasked": {
            "type": "integer",
            "description": "Office files only. Embedded Office files (a workbook behind a chart, a document inside a document), masked with the same token state."
          },
          "embeddedRemoved": {
            "type": "integer",
            "description": "Office files only. Embedded objects that could not be read (OLE objects, other archives, nesting beyond three levels) and were removed."
          },
          "macrosRemoved": {
            "type": "boolean",
            "description": "Office files only. A macro project was removed; the file came back macro-free (`.docm` → `.docx`)."
          },
          "preview": {
            "$ref": "#/components/schemas/FilePreview"
          },
          "receipt": {
            "type": "string",
            "description": "For a masked file: send it back in `fileReceipts` with the next reading of the same file and that reading is not billed again. Valid 10 minutes from the first, billed reading; a free reading returns the same receipt."
          }
        }
      },
      "Attachments": {
        "type": "object",
        "properties": {
          "policy": {
            "type": "string",
            "enum": [
              "block",
              "flag",
              "pass",
              "mask"
            ]
          },
          "masked": {
            "type": "boolean"
          },
          "maskedCount": {
            "type": "integer"
          },
          "references": {
            "type": "integer"
          },
          "inline": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AttachmentItem"
            }
          },
          "note": {
            "type": "string"
          }
        },
        "description": "Files found in the body. `masked` is true only when every inline file came back masked."
      },
      "NameModelStats": {
        "type": "object",
        "properties": {
          "attached": {
            "type": "boolean"
          },
          "engine": {
            "type": "string"
          },
          "mode": {
            "type": "string"
          },
          "fromTicket": {
            "type": "boolean"
          },
          "lang": {
            "type": "string"
          },
          "callCount": {
            "type": "integer"
          }
        }
      },
      "MaskStats": {
        "type": "object",
        "properties": {
          "matches": {
            "type": "integer"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "confidence": {
            "type": "number"
          },
          "ms": {
            "type": "number"
          },
          "nameModel": {
            "$ref": "#/components/schemas/NameModelStats"
          }
        },
        "required": [
          "matches",
          "labels",
          "ms"
        ]
      },
      "MaskTextResponse": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "What to send to the model."
          },
          "map": {
            "$ref": "#/components/schemas/Map"
          },
          "safetyNet": {
            "$ref": "#/components/schemas/SafetyNet"
          },
          "language": {
            "$ref": "#/components/schemas/Language"
          },
          "dictionary": {
            "$ref": "#/components/schemas/DictionaryReport"
          },
          "attachments": {
            "$ref": "#/components/schemas/Attachments"
          },
          "nameTicket": {
            "type": "string"
          },
          "stats": {
            "$ref": "#/components/schemas/MaskStats"
          }
        },
        "required": [
          "text",
          "map",
          "stats"
        ]
      },
      "MaskObjectResponse": {
        "type": "object",
        "properties": {
          "object": {
            "description": "The same structure with values replaced."
          },
          "map": {
            "$ref": "#/components/schemas/Map"
          },
          "language": {
            "$ref": "#/components/schemas/Language"
          },
          "dictionary": {
            "$ref": "#/components/schemas/DictionaryReport"
          },
          "attachments": {
            "$ref": "#/components/schemas/Attachments"
          },
          "stats": {
            "$ref": "#/components/schemas/MaskStats"
          }
        },
        "required": [
          "object",
          "map",
          "stats"
        ]
      },
      "InspectRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "dictionary": {
            "$ref": "#/components/schemas/Dictionary"
          },
          "patterns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomPattern"
            }
          },
          "options": {
            "$ref": "#/components/schemas/Options"
          }
        },
        "required": [
          "text"
        ]
      },
      "Detection": {
        "type": "object",
        "properties": {
          "start": {
            "type": "integer"
          },
          "end": {
            "type": "integer"
          },
          "value": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "patternId": {
            "type": "string"
          },
          "baseConfidence": {
            "type": "number"
          },
          "confidence": {
            "type": "number"
          },
          "validated": {
            "type": "boolean"
          },
          "contextKeyword": {
            "type": "string"
          }
        },
        "required": [
          "start",
          "end",
          "value",
          "token",
          "label",
          "category",
          "patternId",
          "confidence"
        ]
      },
      "InspectResponse": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "map": {
            "$ref": "#/components/schemas/Map"
          },
          "counts": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              },
              "byLabel": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              "byCategory": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            },
            "required": [
              "total",
              "byLabel",
              "byCategory"
            ]
          },
          "detections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Detection"
            }
          },
          "language": {
            "$ref": "#/components/schemas/Language"
          },
          "attachments": {
            "$ref": "#/components/schemas/Attachments"
          },
          "stats": {
            "type": "object",
            "properties": {
              "confidence": {
                "type": "number"
              },
              "ms": {
                "type": "number"
              },
              "labels": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "patterns": {
                "type": "object"
              },
              "nameModel": {
                "$ref": "#/components/schemas/NameModelStats"
              }
            }
          },
          "safetyNet": {
            "type": "object",
            "properties": {
              "clean": {
                "type": "boolean"
              },
              "detail": {
                "type": "string"
              }
            },
            "required": [
              "clean"
            ]
          }
        },
        "required": [
          "text",
          "map",
          "counts",
          "detections",
          "stats",
          "safetyNet"
        ]
      },
      "RestoreRequest": {
        "type": "object",
        "properties": {
          "map": {
            "$ref": "#/components/schemas/Map"
          },
          "text": {
            "type": "string"
          },
          "object": {}
        },
        "required": [
          "map"
        ],
        "description": "Send `text` or `object`. The SDKs restore locally instead, so the map never travels; this route exists for clients that cannot."
      },
      "RestoreResponse": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "object": {}
        }
      },
      "Whoami": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "keyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key's public label. Never the key."
          },
          "tenant": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "statusReason": {
            "type": "string"
          },
          "plan": {
            "type": [
              "string",
              "null"
            ]
          },
          "quota": {
            "type": "object",
            "properties": {
              "monthlyCharacterLimit": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "charactersUsed": {
                "type": "integer"
              },
              "charactersRemaining": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "hardLimit": {
                "type": "boolean"
              },
              "exhausted": {
                "type": "boolean"
              }
            },
            "required": [
              "monthlyCharacterLimit",
              "charactersUsed",
              "charactersRemaining",
              "hardLimit",
              "exhausted"
            ]
          },
          "limits": {
            "type": "object",
            "properties": {
              "requestsPerMinute": {
                "type": "integer"
              },
              "maxBodyBytes": {
                "type": "integer"
              },
              "maxAttachmentBytes": {
                "type": "integer",
                "description": "The largest file `/v1/mask` takes inline, decoded (default 25 MB). A request carrying such a file may exceed `maxBodyBytes` by the file's base64, provided it declares its `Content-Length`; the file is billed and capped by the TEXT it holds — at most `maxBodyBytes` characters per request — never by its bytes."
              }
            },
            "required": [
              "requestsPerMinute",
              "maxBodyBytes"
            ]
          },
          "masking": {
            "type": "object",
            "properties": {
              "coreference": {
                "type": "boolean"
              },
              "inflection": {
                "type": "string",
                "enum": [
                  "off",
                  "tag"
                ],
                "description": "What the account runs under: `tag` unless the administrator switched case tags off (the default since 22.10.2026)."
              },
              "inflectionLanguage": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "inflectionLanguages": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string"
                },
                "description": "The languages the account's case tags are read in: the administrator's choice, or ALL of `catalogue.inflectionLanguages` for an account that never chose (the default since 22.10.2026). Null when the account declared one `inflectionLanguage`, switched case tags off without choosing languages, or reads its language off the text (`inferLanguage`). They outrank `inflectionLanguage`."
              },
              "enabledGroups": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The opt-in groups the account runs under: its own `patterns.enableGroups` (empty means none), or `catalogue.defaultGroups` for an account that never chose."
              },
              "countries": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string"
                }
              },
              "customPatterns": {
                "type": "boolean"
              },
              "attachmentPolicy": {
                "type": "string",
                "enum": [
                  "block",
                  "flag",
                  "pass",
                  "mask"
                ]
              }
            }
          },
          "instance": {
            "type": "object",
            "properties": {
              "proxy": {
                "description": "`false`, or `{dialect}` when this instance proxies a model."
              },
              "nameModel": {
                "type": "boolean"
              },
              "mcp": {
                "type": "boolean"
              },
              "ocr": {
                "type": "boolean",
                "description": "Whether this instance has the OCR sidecar."
              },
              "convert": {
                "type": "boolean",
                "description": "Whether this instance has the converter of old binary Office files (.doc, .xls, .ppt)."
              }
            }
          },
          "connect": {
            "type": "object",
            "description": "Ready-to-paste fragments for the MCP connector, the extension and the three SDKs."
          },
          "catalogue": {
            "type": "object",
            "properties": {
              "categories": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "labels": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "countries": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "inflectionLanguages": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "groups": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Opt-in groups a tenant may switch on (`special.health`, `scope.dates`); a family name (`special`) switches on every group under it."
              },
              "defaultGroups": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The groups ticked for an account that never chose: the device identifiers (`devices.identifiers` — IMEI, ICCID, MEID, MAC address, label `Device`). An administrator unticks them like any other group; an explicit `patterns.enableGroups`, an empty list included, is the account's exact choice."
              },
              "groupNames": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "What each opt-in group masks, in a customer's words, keyed by group."
              },
              "presets": {
                "type": "array",
                "description": "Sector presets: a named set of opt-in groups for one sector. A preset fills `patterns.enableGroups`. The gateway reads one thing from it: `attachmentPolicy`, the file policy a key runs under when it never chose one and its enabled groups are exactly the preset's.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "attachmentPolicy": {
                      "type": "string",
                      "enum": [
                        "block",
                        "flag",
                        "pass",
                        "mask"
                      ],
                      "description": "The file policy this sector implies. Absent: the default."
                    }
                  },
                  "required": [
                    "id",
                    "label",
                    "description",
                    "groups"
                  ]
                }
              }
            },
            "description": "What there is to narrow, read off the rule catalogue this instance loaded."
          },
          "accountUrl": {
            "type": "string",
            "format": "uri"
          },
          "ocr": {
            "type": "object",
            "description": "OCR for this key: scans, photos and PDFs with no text layer are read and masked; every paid plan has it, Free does not. `available` is false when the plan has it and this instance has no OCR sidecar. `monthlyPageLimit` is a monthly page CAP that protects the OCR service's capacity — not an allowance and not a price: the text OCR reads is billed in characters, like any other text, in the pool counted under `quota`. The cap applies whatever `hardLimit` says.",
            "properties": {
              "entitled": {
                "type": "boolean"
              },
              "available": {
                "type": "boolean"
              },
              "monthlyPageLimit": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "pagesUsed": {
                "type": "integer"
              },
              "exhausted": {
                "type": "boolean"
              }
            },
            "required": [
              "entitled",
              "available",
              "monthlyPageLimit",
              "pagesUsed",
              "exhausted"
            ]
          },
          "contentLog": {
            "type": "object",
            "description": "Whether THIS gateway records request content. Only a customer-operated gateway ever answers `enabled: true`; the browser add-on sends file contents only to one that does.",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "retentionDays": {
                "type": "integer"
              }
            },
            "required": [
              "enabled"
            ]
          }
        },
        "required": [
          "ok",
          "tenant",
          "status",
          "quota",
          "limits",
          "masking",
          "instance",
          "ocr"
        ]
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "snapshot stale"
            ]
          },
          "startedAt": {
            "type": "string",
            "format": "date-time"
          },
          "snapshot": {
            "type": "object"
          },
          "sharedLimit": {
            "type": "boolean"
          },
          "nameModel": {
            "type": "object",
            "properties": {
              "attached": {
                "type": "boolean"
              },
              "socket": {
                "type": "string",
                "enum": [
                  "present",
                  "missing"
                ]
              }
            },
            "required": [
              "attached"
            ]
          },
          "usage": {
            "type": "object",
            "properties": {
              "sink": {
                "type": "string"
              },
              "durable": {
                "type": "boolean"
              },
              "sent": {
                "type": "integer"
              },
              "failed": {
                "type": "integer"
              },
              "down": {
                "type": "boolean"
              },
              "seq": {
                "type": "integer"
              }
            },
            "required": [
              "sink"
            ],
            "description": "Whether usage reports reach the control plane. Numbers only."
          },
          "ocr": {
            "type": "object",
            "description": "The OCR sidecar: counts and the engine's name, never a page.",
            "properties": {
              "attached": {
                "type": "boolean"
              },
              "engine": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "calls": {
                "type": "integer"
              },
              "failures": {
                "type": "integer"
              },
              "pages": {
                "type": "integer"
              }
            },
            "required": [
              "attached"
            ]
          },
          "convert": {
            "type": "object",
            "description": "The converter of old binary Office files (.doc, .xls, .ppt): counts and the engine's name, never a file.",
            "properties": {
              "attached": {
                "type": "boolean"
              },
              "engine": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "calls": {
                "type": "integer"
              },
              "failures": {
                "type": "integer"
              }
            },
            "required": [
              "attached"
            ]
          }
        },
        "required": [
          "status",
          "startedAt",
          "snapshot",
          "nameModel",
          "usage",
          "ocr"
        ]
      },
      "ProviderBody": {
        "type": "object",
        "description": "The provider's own request body, unchanged in shape. A `dictionary` field may be added; it is used and removed before the request goes on."
      },
      "ExtensionEvent": {
        "type": "object",
        "description": "One event. `type` and `at` always; at most the fields listed for its type: `heartbeat` → `extensionVersion`, `managed`, `policy`, `enabled`; `site-added`/`site-removed` → `host`; `file-unmasked` → `fileType`, `channel`; `settings-changed`/`settings-reverted` → `settings`; `dictionary-loaded` → `entries`; every other type → none.",
        "required": [
          "type",
          "at"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "heartbeat",
              "paused",
              "resumed",
              "site-added",
              "site-removed",
              "file-unmasked",
              "admin-created",
              "admin-sign-in",
              "admin-sign-in-failed",
              "admin-password-changed",
              "admin-removed",
              "settings-changed",
              "settings-reverted",
              "dictionary-loaded",
              "dictionary-removed",
              "config-imported"
            ]
          },
          "at": {
            "type": "integer",
            "description": "The browser's clock, milliseconds since the epoch."
          },
          "extensionVersion": {
            "type": "string"
          },
          "managed": {
            "type": "boolean"
          },
          "policy": {
            "type": "boolean",
            "description": "Whether the organisation's browser policy sets any of the add-on's settings — never which values."
          },
          "enabled": {
            "type": "boolean"
          },
          "host": {
            "type": "string",
            "description": "A chat site's hostname."
          },
          "fileType": {
            "type": "string",
            "description": "A file type from a closed list (`pdf`, `docx`, … `other`, `none`) — never a file name."
          },
          "channel": {
            "type": "string",
            "enum": [
              "picker",
              "drop",
              "paste"
            ]
          },
          "settings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of settings — never their values."
          },
          "entries": {
            "type": "integer",
            "minimum": 0,
            "maximum": 20000
          }
        }
      },
      "ExtensionEvents": {
        "type": "object",
        "required": [
          "installation",
          "events"
        ],
        "properties": {
          "installation": {
            "type": "string",
            "description": "A random 26-character id the extension made at install."
          },
          "events": {
            "type": "array",
            "maxItems": 100,
            "items": {
              "$ref": "#/components/schemas/ExtensionEvent"
            }
          }
        }
      },
      "ExtensionEventsAccepted": {
        "type": "object",
        "required": [
          "accepted",
          "keyId",
          "contentLog"
        ],
        "properties": {
          "accepted": {
            "type": "integer"
          },
          "keyId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key's public id, for the extension's uninstall address — never the key."
          },
          "contentLog": {
            "type": "boolean",
            "description": "Whether this gateway records request content (a customer-operated gateway only). The add-on learns it here, at every browser start."
          }
        }
      },
      "ProviderError": {
        "type": "object",
        "description": "An error body in the model provider's own shape, as the provider wrote it: Anthropic `{type: \"error\", error: {type, message}}`, OpenAI and Mistral `{error: {message, type, param, code}}`, Gemini `{error: {code, message, status}}`. Tokens in it are NOT restored. If the provider's body was not JSON (a CDN's error page) or was larger than 32 KiB, the status is still the provider's and the gateway writes a fixed sentence in the same shape, saying so in the message."
      },
      "ProviderOrGatewayError": {
        "type": "object",
        "description": "The gateway's own `Error`, or — when the answer carries `x-obvelo-upstream: provider` — the model provider's error body (`ProviderError`)."
      },
      "FilePreview": {
        "type": "object",
        "description": "A masked file's text as the returned file holds it. One segment per reading: the whole file (an Office file's parts are its lines, in reading order) or an embedded file inside it. Present only when the request set `attachmentPreview`.",
        "properties": {
          "segments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string",
                  "description": "The text after masking; tokens stand where values stood."
                },
                "tokens": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "start": {
                        "type": "integer",
                        "description": "Offset of the token in `text`."
                      },
                      "end": {
                        "type": "integer"
                      },
                      "token": {
                        "type": "string"
                      },
                      "label": {
                        "type": "string",
                        "description": "The category the token names, e.g. `Person`."
                      },
                      "value": {
                        "type": "string",
                        "description": "What the token replaced, from the answer's map. Absent when no map is kept."
                      }
                    },
                    "required": [
                      "start",
                      "end",
                      "token",
                      "label"
                    ]
                  }
                }
              },
              "required": [
                "text",
                "tokens"
              ]
            }
          }
        },
        "required": [
          "segments"
        ]
      },
      "ContentEvents": {
        "type": "object",
        "required": [
          "installation",
          "events"
        ],
        "properties": {
          "installation": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "object",
              "description": "A `file-unmasked` event: `type`, `at`, `channel`, `fileType`, `name`, `size`, and optionally `host`, `mediaType` and `content` (base64, at most 5 MiB decoded). Nothing else is accepted."
            }
          }
        }
      },
      "ContentEventsAccepted": {
        "type": "object",
        "required": [
          "accepted"
        ],
        "properties": {
          "accepted": {
            "type": "integer"
          }
        }
      },
      "AddonPolicyAnswer": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "configured": {
            "type": "boolean",
            "description": "Whether anybody ever saved a policy for this key's account or this instance."
          },
          "source": {
            "type": "string",
            "enum": [
              "account",
              "instance",
              "none"
            ]
          },
          "revision": {
            "type": "string",
            "description": "A short digest of the policy in force."
          },
          "policy": {
            "type": "object",
            "properties": {
              "promptUnmasking": {
                "type": "string",
                "enum": [
                  "allow",
                  "forbid"
                ]
              },
              "fileUnmasking": {
                "type": "string",
                "enum": [
                  "allow",
                  "forbid"
                ]
              },
              "unmaskableFiles": {
                "type": "string",
                "enum": [
                  "warn",
                  "block"
                ]
              },
              "livePreview": {
                "type": "boolean"
              },
              "maskOnEnter": {
                "type": "boolean"
              },
              "tokenInstruction": {
                "type": "boolean"
              },
              "sites": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "blockUntilConfigured": {
                "type": "boolean"
              },
              "uiLanguage": {
                "type": "string",
                "enum": [
                  "en",
                  "de",
                  "pl"
                ],
                "description": "Language of what users and administrators read; carried, not yet applied to any text."
              }
            },
            "required": [
              "promptUnmasking",
              "fileUnmasking",
              "unmaskableFiles",
              "livePreview",
              "maskOnEnter",
              "tokenInstruction",
              "sites",
              "blockUntilConfigured",
              "uiLanguage"
            ]
          },
          "scope": {
            "type": "object",
            "properties": {
              "countries": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string"
                }
              },
              "enabledGroups": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "countries",
              "enabledGroups"
            ]
          },
          "accountUrl": {
            "type": "string"
          },
          "override": {
            "type": [
              "object",
              "null"
            ],
            "description": "The settings this browser has of its own (names only, never values), or null.",
            "properties": {
              "keys": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "keys"
            ]
          },
          "dictionary": {
            "type": [
              "object",
              "null"
            ],
            "description": "The organisation's central dictionary on a customer-operated gateway (revision and entry count), or null. Fetch it from /v1/addon-dictionary when the revision differs from the copy held.",
            "properties": {
              "revision": {
                "type": "string"
              },
              "entries": {
                "type": "integer"
              }
            },
            "required": [
              "revision",
              "entries"
            ]
          }
        },
        "required": [
          "ok",
          "configured",
          "source",
          "revision",
          "policy",
          "scope"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The body is not what this route takes. `error` says which field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No key, or a key this gateway does not know. `hint` says which, and where to put it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The plan's monthly characters are spent, or the subscription is unpaid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "This key may not do this: the account is suspended, or the plan lacks the feature.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "The body is over this key's limit (`maxKB` and `suggestedParts` say how to split it), an attached file is over the per-file limit (`maxAttachmentBytes`), or the text in the attached files is over the key's per-request limit (`maxCharacters`), or one text is too long for the name model to read in time (`code` is `text-too-long-for-name-model`). Each carries a `hint` naming the remedy.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests a minute.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "retry-after": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          }
        }
      },
      "SnapshotStale": {
        "description": "The instance lost touch with the control plane too long ago to trust its keys. On `/v1/mask`, also: the instance is busy with other large files (`retryAfterSeconds`, with a `hint`). Also, on any route that masks (`/v1/mask`, `/v1/inspect`, the proxy dialects, `/mcp`): the document took longer to mask than the operator's processing budget (`ENGINE_TIME_BUDGET_MS`, default 60 s per engine call) — `code` is `engine-budget-exceeded`, `budgetMs` the budget, and nothing masked or unmasked is returned or forwarded. Or, on the same routes, a call to the name model failed (`code` is `name-model-unavailable`, with a `hint`): the request is refused rather than masked by the rules alone, because a name only the model knows would have gone on in clear; nothing is returned, forwarded or billed. It applies only where the model is in use for the request — a tenant or request with `ner: off`, and an instance with no model, are never refused.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ProxyBadRequest": {
        "description": "The body is not what this route takes (the gateway's `Error`; `error` says which field), or the model provider refused the request as invalid (its own error body, marked `x-obvelo-upstream: provider`). Also kept when the provider sends them: its request id (`request-id` at Anthropic, `x-request-id` and `apim-request-id` at OpenAI-compatible providers) and its rate-limit headers (`anthropic-ratelimit-*`, `x-ratelimit-*`, `ratelimit-*`). Nothing else of the provider's headers is passed on.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProviderOrGatewayError"
            }
          }
        },
        "headers": {
          "x-obvelo-upstream": {
            "description": "Present, with the value `provider`, when the answer is the model provider's own (its status and its body). Absent on the gateway's own refusals, so a 401 or a 429 without it is the gateway's.",
            "schema": {
              "type": "string",
              "enum": [
                "provider"
              ]
            }
          },
          "retry-after": {
            "description": "The provider's own, kept as it came (seconds or a date).",
            "schema": {
              "type": "string"
            }
          },
          "retry-after-ms": {
            "description": "The provider's own, in milliseconds.",
            "schema": {
              "type": "string"
            }
          },
          "x-should-retry": {
            "description": "The provider's own verdict on retrying, which its client library honours.",
            "schema": {
              "type": "string"
            }
          },
          "request-id": {
            "description": "The provider's request id as Anthropic sends it, when it sent one.",
            "schema": {
              "type": "string"
            }
          },
          "x-request-id": {
            "description": "The provider's request id as OpenAI-compatible providers send it, when one was sent.",
            "schema": {
              "type": "string"
            }
          },
          "apim-request-id": {
            "description": "Azure OpenAI's request id, when one was sent.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "ProxyUnauthorized": {
        "description": "Without `x-obvelo-upstream`: no key, or a key this gateway does not know (`hint` says which, and where to put it). With `x-obvelo-upstream: provider`: the model provider refused the PROVIDER's key, with its own error body. Also kept when the provider sends them: its request id (`request-id` at Anthropic, `x-request-id` and `apim-request-id` at OpenAI-compatible providers) and its rate-limit headers (`anthropic-ratelimit-*`, `x-ratelimit-*`, `ratelimit-*`). Nothing else of the provider's headers is passed on.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProviderOrGatewayError"
            }
          }
        },
        "headers": {
          "x-obvelo-upstream": {
            "description": "Present, with the value `provider`, when the answer is the model provider's own (its status and its body). Absent on the gateway's own refusals, so a 401 or a 429 without it is the gateway's.",
            "schema": {
              "type": "string",
              "enum": [
                "provider"
              ]
            }
          },
          "retry-after": {
            "description": "The provider's own, kept as it came (seconds or a date).",
            "schema": {
              "type": "string"
            }
          },
          "retry-after-ms": {
            "description": "The provider's own, in milliseconds.",
            "schema": {
              "type": "string"
            }
          },
          "x-should-retry": {
            "description": "The provider's own verdict on retrying, which its client library honours.",
            "schema": {
              "type": "string"
            }
          },
          "request-id": {
            "description": "The provider's request id as Anthropic sends it, when it sent one.",
            "schema": {
              "type": "string"
            }
          },
          "x-request-id": {
            "description": "The provider's request id as OpenAI-compatible providers send it, when one was sent.",
            "schema": {
              "type": "string"
            }
          },
          "apim-request-id": {
            "description": "Azure OpenAI's request id, when one was sent.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "ProxyForbidden": {
        "description": "Without `x-obvelo-upstream`: this key may not do this (the model proxy switched off for the key, a tool outside the allowlist, a plan without the feature). With `x-obvelo-upstream: provider`: the provider refused, with its own error body. Also kept when the provider sends them: its request id (`request-id` at Anthropic, `x-request-id` and `apim-request-id` at OpenAI-compatible providers) and its rate-limit headers (`anthropic-ratelimit-*`, `x-ratelimit-*`, `ratelimit-*`). Nothing else of the provider's headers is passed on.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProviderOrGatewayError"
            }
          }
        },
        "headers": {
          "x-obvelo-upstream": {
            "description": "Present, with the value `provider`, when the answer is the model provider's own (its status and its body). Absent on the gateway's own refusals, so a 401 or a 429 without it is the gateway's.",
            "schema": {
              "type": "string",
              "enum": [
                "provider"
              ]
            }
          },
          "retry-after": {
            "description": "The provider's own, kept as it came (seconds or a date).",
            "schema": {
              "type": "string"
            }
          },
          "retry-after-ms": {
            "description": "The provider's own, in milliseconds.",
            "schema": {
              "type": "string"
            }
          },
          "x-should-retry": {
            "description": "The provider's own verdict on retrying, which its client library honours.",
            "schema": {
              "type": "string"
            }
          },
          "request-id": {
            "description": "The provider's request id as Anthropic sends it, when it sent one.",
            "schema": {
              "type": "string"
            }
          },
          "x-request-id": {
            "description": "The provider's request id as OpenAI-compatible providers send it, when one was sent.",
            "schema": {
              "type": "string"
            }
          },
          "apim-request-id": {
            "description": "Azure OpenAI's request id, when one was sent.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "ProxyRateLimited": {
        "description": "Without `x-obvelo-upstream`: too many requests a minute for this key; `retry-after` is the gateway's. With `x-obvelo-upstream: provider`: the model provider's rate limit, with the provider's own `retry-after` and error body. Also kept when the provider sends them: its request id (`request-id` at Anthropic, `x-request-id` and `apim-request-id` at OpenAI-compatible providers) and its rate-limit headers (`anthropic-ratelimit-*`, `x-ratelimit-*`, `ratelimit-*`). Nothing else of the provider's headers is passed on.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProviderOrGatewayError"
            }
          }
        },
        "headers": {
          "x-obvelo-upstream": {
            "description": "Present, with the value `provider`, when the answer is the model provider's own (its status and its body). Absent on the gateway's own refusals, so a 401 or a 429 without it is the gateway's.",
            "schema": {
              "type": "string",
              "enum": [
                "provider"
              ]
            }
          },
          "retry-after": {
            "description": "The provider's own, kept as it came (seconds or a date).",
            "schema": {
              "type": "string"
            }
          },
          "retry-after-ms": {
            "description": "The provider's own, in milliseconds.",
            "schema": {
              "type": "string"
            }
          },
          "x-should-retry": {
            "description": "The provider's own verdict on retrying, which its client library honours.",
            "schema": {
              "type": "string"
            }
          },
          "request-id": {
            "description": "The provider's request id as Anthropic sends it, when it sent one.",
            "schema": {
              "type": "string"
            }
          },
          "x-request-id": {
            "description": "The provider's request id as OpenAI-compatible providers send it, when one was sent.",
            "schema": {
              "type": "string"
            }
          },
          "apim-request-id": {
            "description": "Azure OpenAI's request id, when one was sent.",
            "schema": {
              "type": "string"
            }
          }
        }
      },
      "ProviderError": {
        "description": "The model provider answered with an error status, and the gateway passed it on: the provider's own status (any 4xx or 5xx, for instance 404, 413, 500 or Anthropic's 529), its own error body and the headers its client library retries by. The provider's official library raises its usual typed error. Also kept when the provider sends them: its request id (`request-id` at Anthropic, `x-request-id` and `apim-request-id` at OpenAI-compatible providers) and its rate-limit headers (`anthropic-ratelimit-*`, `x-ratelimit-*`, `ratelimit-*`). Nothing else of the provider's headers is passed on.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProviderError"
            }
          }
        },
        "headers": {
          "x-obvelo-upstream": {
            "description": "Present, with the value `provider`, when the answer is the model provider's own (its status and its body). Absent on the gateway's own refusals, so a 401 or a 429 without it is the gateway's.",
            "schema": {
              "type": "string",
              "enum": [
                "provider"
              ]
            }
          },
          "retry-after": {
            "description": "The provider's own, kept as it came (seconds or a date).",
            "schema": {
              "type": "string"
            }
          },
          "retry-after-ms": {
            "description": "The provider's own, in milliseconds.",
            "schema": {
              "type": "string"
            }
          },
          "x-should-retry": {
            "description": "The provider's own verdict on retrying, which its client library honours.",
            "schema": {
              "type": "string"
            }
          },
          "request-id": {
            "description": "The provider's request id as Anthropic sends it, when it sent one.",
            "schema": {
              "type": "string"
            }
          },
          "x-request-id": {
            "description": "The provider's request id as OpenAI-compatible providers send it, when one was sent.",
            "schema": {
              "type": "string"
            }
          },
          "apim-request-id": {
            "description": "Azure OpenAI's request id, when one was sent.",
            "schema": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
