{
  "openapi": "3.1.0",
  "info": {
    "title": "Secure AI",
    "version": "1.0.0",
    "description": "A redaction API. Finds the people in a piece of text — names, emails, phone numbers, addresses, card and account numbers — and swaps them for plausible stand-ins before the text reaches a language model, then puts the real values back in the model's reply.\n\nThe substitution is the point rather than a detail: a model handed [NAME] writes about [NAME] and the answer comes back needing repair, where a model handed a name writes an ordinary sentence.\n\nThe map between a stand-in and a real value is returned with the response and never stored. A caller who loses one cannot restore that answer.",
    "contact": { "url": "https://secureai.one/developers" }
  },
  "servers": [{ "url": "https://secureai.one/v1" }],
  "security": [{ "apiKey": [] }],
  "paths": {
    "/redact": {
      "post": {
        "operationId": "redact",
        "summary": "Take identifying detail out of text",
        "description": "Returns the text with people replaced by plausible stand-ins, and the map needed to reverse it. Send the returned text to your model; send the model's reply to /restore with the same map.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RedactRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The redacted text and its map",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Redaction" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "413": { "$ref": "#/components/responses/TooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/RedactionUnavailable" }
        }
      }
    },
    "/restore": {
      "post": {
        "operationId": "restore",
        "summary": "Put the real values back",
        "description": "Applies a map in reverse. Use it on a model's reply so the reader sees what they actually wrote. An empty map is not an error: text with nothing personal in it round-trips unchanged.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RestoreRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The text with real values restored",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Restoration" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/proxy/chat/completions": {
      "post": {
        "operationId": "proxyChatCompletion",
        "summary": "Redact, call your model, restore — one request",
        "description": "Takes an OpenAI-shaped chat request, redacts it, forwards it to your own model vendor using the key in X-Provider-Key, and restores the real values in the reply. Your provider key is forwarded to that one call and never stored. OpenAI-compatible, so an existing client needs only a new base URL.",
        "parameters": [
          {
            "name": "X-Provider-Key",
            "in": "header",
            "required": true,
            "description": "Your own model vendor's API key. Forwarded, never stored.",
            "schema": { "type": "string" }
          },
          {
            "name": "X-Provider",
            "in": "header",
            "required": false,
            "description": "Which vendor, when the model name does not say. One of anthropic, openai, google.",
            "schema": { "type": "string", "enum": ["anthropic", "openai", "google"] }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ChatRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An OpenAI chat completion, with real values restored",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatCompletion" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "$ref": "#/components/responses/RedactionUnavailable" }
        }
      }
    },
    "/chat/completions": {
      "post": {
        "operationId": "chatCompletion",
        "summary": "The same, using our models",
        "description": "As the proxy, but answered by our own provider accounts and metered against a prepaid balance. No X-Provider-Key. The model may be omitted, in which case one is chosen by how hard the question is.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ChatRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An OpenAI chat completion",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ChatCompletion" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "usage",
        "summary": "What this account has used this month",
        "responses": {
          "200": {
            "description": "Balance and requests",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Usage" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A key created in Settings → Developer. Shown once; what is stored is its SHA-256."
      }
    },
    "schemas": {
      "RedactRequest": {
        "type": "object",
        "required": ["text"],
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 200000,
            "description": "The text to redact."
          },
          "map": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "A map from a previous call, so the same person keeps the same stand-in across turns."
          },
          "allow": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Terms to send as written — a shared mailbox, a product name a detector mistakes for a person."
          },
          "profile": {
            "type": "object",
            "description": "The end user's own details, so they are recognised even when written unusually.",
            "properties": {
              "name": { "type": "string" },
              "email": { "type": "string" },
              "phone": { "type": "string" }
            }
          },
          "strict": {
            "type": "boolean",
            "default": false,
            "description": "Refuse the request rather than proceed when the name check cannot decide. Off by default: the structured stages have already run, and refusing costs somebody their answer to protect a name that may not be there. Turn it on when the input is records rather than one person's messages."
          }
        }
      },
      "Redaction": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "redaction" },
          "text": { "type": "string", "description": "The text with stand-ins in place. Send this to your model." },
          "map": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Stand-in to real value. Returned here and stored nowhere — keep it for the life of the conversation or that answer cannot be restored."
          },
          "redacted": { "type": "integer", "description": "How many values were replaced." },
          "names_decided": {
            "type": "boolean",
            "description": "Whether the name check reached a decision. False means it could not run, not that no names were found."
          }
        }
      },
      "RestoreRequest": {
        "type": "object",
        "required": ["text", "map"],
        "properties": {
          "text": { "type": "string", "description": "Usually your model's reply." },
          "map": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "The map from the matching /redact call."
          }
        }
      },
      "Restoration": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "restoration" },
          "text": { "type": "string" },
          "restored": { "type": "integer" }
        }
      },
      "ChatRequest": {
        "type": "object",
        "required": ["messages"],
        "properties": {
          "model": { "type": "string", "description": "Required on the proxy; optional on /chat/completions." },
          "messages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "role": { "type": "string", "enum": ["system", "user", "assistant"] },
                "content": { "type": "string" }
              }
            }
          },
          "stream": { "type": "boolean", "default": false },
          "map": { "type": "object", "additionalProperties": { "type": "string" } },
          "allow": { "type": "array", "items": { "type": "string" } },
          "strict": { "type": "boolean", "default": false }
        }
      },
      "ChatCompletion": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "object": { "type": "string", "const": "chat.completion" },
          "created": { "type": "integer" },
          "model": { "type": "string" },
          "choices": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": { "type": "integer" },
                "message": {
                  "type": "object",
                  "properties": {
                    "role": { "type": "string" },
                    "content": { "type": "string" }
                  }
                },
                "finish_reason": { "type": "string", "enum": ["stop", "length"] }
              }
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "prompt_tokens": { "type": "integer" },
              "completion_tokens": { "type": "integer" },
              "total_tokens": { "type": "integer" }
            }
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "object": { "type": "string", "const": "usage" },
          "balance_usd": { "type": "number" },
          "spent_usd": { "type": "number" },
          "remaining_usd": { "type": "number" },
          "requests_this_month": { "type": "integer" },
          "requests_included": { "type": "integer" },
          "tier": { "type": "string" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "message": { "type": "string" },
              "type": { "type": "string" },
              "code": { "type": ["string", "null"] },
              "param": { "type": ["string", "null"] }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was wrong — no model named, no provider key, a body that is not JSON",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "No API key, or one that is not recognised",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PaymentRequired": {
        "description": "No API access on this account, or out of balance. The code says which.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "The plan does not include that model",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "TooLarge": {
        "description": "More than 200,000 characters in one call",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "60 requests a minute per key, or a spending cap reached",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RedactionUnavailable": {
        "description": "Redaction could not run, so nothing was sent to any model",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  }
}
