{
 "openapi": "3.1.0",
 "info": {
  "title": "Tayanch API",
  "version": "2026-09-24",
  "description": "AI-generated media detection. Replies are JSON; the key set of every reply is pinned by contract/api_v2.json and only grows (append-only). Every verdict carries model and config_version. Errors: 400 Bad request: no file in the expected field, empty upload, undecodable media, bad JSON, or a sealed request with a stale timestamp (server_time tells the right clock).; 401 Missing API key (send X-API-Key), expired site token, or not signed in (/account).; 402 The key's quota is used up.; 403 Invalid, revoked or expired API key.; 409 Sealed request made to a rotated key (reseal with the returned pubkey), or the account / key limit already exists.; 410 heatmap_token expired (30 minutes) — call /analyze again.; 413 File too large (image 40 MB, upload 200 MB) or over the image pixel limit.; 415 Unsupported file type.; 422 The link is not a fetchable http(s) URL, or the platform refused the fetch.; 429 Rate limit (free keys 30 requests/min) — retry after a minute.; 500 The analysis failed on our side; the file is discarded. Retry once, then contact support.; 503 Modality under maintenance (status: under_maintenance), the daily capacity is reached, the server is busy, or a model is still warming up after idle. Retry later.; 504 A link check passed its 90 s deadline: code timeout, retryable. Retry, or upload the file directly.. Idempotency-Key header (billable POSTs, 1-128 printable characters): the same key on the same API key and route replays the first successful reply for 24 hours (Idempotent-Replayed: true) and is charged once; the same key with a different body is refused (422, code idempotency_key_reused)."
 },
 "servers": [
  {
   "url": "https://jamoliddinbekyodgorov--tayanch-detector-tayanch-serve.modal.run"
  }
 ],
 "components": {
  "securitySchemes": {
   "apiKey": {
    "type": "apiKey",
    "in": "header",
    "name": "X-API-Key"
   },
   "bearer": {
    "type": "http",
    "scheme": "bearer"
   }
  },
  "schemas": {
   "PreOpenError": {
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "code": {
        "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
       },
       "error": {
        "description": "Machine-readable or human-readable error string."
       },
       "request_id": {
        "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
       },
       "retryable": {
        "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
       },
       "server_time": {
        "description": "Pre-open error: the server clock, so the client can correct a stale timestamp."
       }
      },
      "title": "400 stale_timestamp"
     },
     {
      "type": "object",
      "properties": {
       "code": {
        "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
       },
       "error": {
        "description": "Machine-readable or human-readable error string."
       },
       "kid": {
        "description": "Key id of the server key the envelope must be sealed to."
       },
       "pubkey": {
        "description": "Pre-open error: the current public key document, so the client can reseal."
       },
       "request_id": {
        "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
       },
       "retryable": {
        "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
       }
      },
      "title": "409 key_mismatch"
     }
    ]
   }
  }
 },
 "paths": {
  "/health": {
   "get": {
    "summary": "Service health and which modalities are live.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "audio_head": {
           "description": "Name of the served audio head, or null."
          },
          "build": {
           "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
          },
          "checks": {
           "description": "Health: per-component readiness."
          },
          "container_started": {
           "description": "Health: Unix time the serving container started."
          },
          "email_sender": {
           "description": "Whether e-mail actions work: {configured, support}. When configured is false, password reset is unavailable; write to support."
          },
          "link_auth": {
           "description": "Health: true when link video downloads run signed in to the platforms (yt-dlp cookies present); false means Instagram/TikTok reels may fall back to the preview picture."
          },
          "modalities": {
           "description": "Health: image / video / audio each 'live' or 'maintenance'; audio is 'preview' while its head has no accepted calibration (replies carry no verdict)."
          },
          "model": {
           "description": "Name of the head that produced the score."
          },
          "ready": {
           "description": "Health: true once the image model is built on this container (a cold container answers /health before it is warm)."
          },
          "request_errors": {
           "description": "Health: {count, last_seen} of unhandled server errors on this container. A counter, not a failed check."
          },
          "status": {
           "description": "Health: ok, or degraded when any entry in checks.failed is set (HTTP stays 200). Maintenance replies: under_maintenance."
          }
         },
         "title": "default"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/pubkey": {
   "get": {
    "summary": "The server's current public key for sealing uploads (TSE2). Query: nonce (64 hex chars).",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "attestation": {
           "description": "Attested tier only: the evidence the client checks before sealing to this key; null on the standard tier."
          },
          "client": {
           "description": "Public key document: the client versions this envelope suite supports."
          },
          "envelope": {
           "description": "Public key document: the sealed-envelope format (TSE2)."
          },
          "key_mode": {
           "description": "Public key document: how the server key is held (ephemeral, env or file)."
          },
          "kid": {
           "description": "Key id of the server key the envelope must be sealed to."
          },
          "minted_at": {
           "description": "Public key document: when this key was created, Unix time."
          },
          "not_after": {
           "description": "Public key document: the key is not accepted after this Unix time."
          },
          "public_key": {
           "description": "Public key document: the server's X25519 public key, base64."
          },
          "suite": {
           "description": "Public key document: HPKE suite (X25519, HKDF-SHA256, AES-256-GCM)."
          },
          "tier": {
           "description": "Public key document: standard or attested."
          },
          "version": {
           "description": "Public key document: format version."
          }
         },
         "title": "default"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/analyze": {
   "post": {
    "summary": "Analyse one image.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "decided_by": {
             "description": "Which head produced p_ai and the verdict: the primary image model, the web-copy sidecar, a fallback candidate, or provenance."
            },
            "degradation": {
             "description": "Image: how much forensic signal the file has left — level (none, moderate, heavy), reasons (e.g. recompressed, resized, no camera EXIF), size and format."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "heatmap_token": {
             "description": "Image: pass back to POST /heatmap within 30 minutes to get the region map without re-sending the verdict."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "preview": {
             "description": "Image: a small (<= 360 px) JPEG data URL of the analysed image; omitted when the request sends preview=0."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "content"
          },
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "decided_by": {
             "description": "Which head produced p_ai and the verdict: the primary image model, the web-copy sidecar, a fallback candidate, or provenance."
            },
            "degradation": {
             "description": "Image: how much forensic signal the file has left — level (none, moderate, heavy), reasons (e.g. recompressed, resized, no camera EXIF), size and format."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "heatmap_token": {
             "description": "Image: pass back to POST /heatmap within 30 minutes to get the region map without re-sending the verdict."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "localized_edit": {
             "description": "Image judged real: the edited-photo screen's result {score, thr, fires}; fires=true turns the verdict into 'Uncertain — possible local edit'."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "preview": {
             "description": "Image: a small (<= 360 px) JPEG data URL of the analysed image; omitted when the request sends preview=0."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "content+edit_screen"
          },
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "decided_by": {
             "description": "Which head produced p_ai and the verdict: the primary image model, the web-copy sidecar, a fallback candidate, or provenance."
            },
            "degradation": {
             "description": "Image: how much forensic signal the file has left — level (none, moderate, heavy), reasons (e.g. recompressed, resized, no camera EXIF), size and format."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "heatmap_token": {
             "description": "Image: pass back to POST /heatmap within 30 minutes to get the region map without re-sending the verdict."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "mix": {
             "description": "Present only on some uncertain replies: 'Mixed signals' — ai_share and real_share (each model's read on its own calibrated band), every model's read, and the parts (time segments or an image region). The verdict stays uncertain."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "preview": {
             "description": "Image: a small (<= 360 px) JPEG data URL of the analysed image; omitted when the request sends preview=0."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "content uncertain"
          },
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "decided_by": {
             "description": "Which head produced p_ai and the verdict: the primary image model, the web-copy sidecar, a fallback candidate, or provenance."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "heatmap_token": {
             "description": "Image: pass back to POST /heatmap within 30 minutes to get the region map without re-sending the verdict."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "preview": {
             "description": "Image: a small (<= 360 px) JPEG data URL of the analysed image; omitted when the request sends preview=0."
            },
            "provenance_kind": {
             "description": "Provenance replies: which marker was found (e.g. C2PA digitalSourceType)."
            },
            "provenance_verified": {
             "description": "Provenance replies: true when the C2PA signature was cryptographically verified."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "provenance"
          }
         ]
        }
       }
      }
     },
     "400": {
      "description": "Bad request: no file in the expected field, empty upload, undecodable media, bad JSON, or a sealed request with a stale timestamp (server_time tells the right clock).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "no_file"
        }
       }
      }
     }
    },
    "security": [
     {
      "apiKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "properties": {
         "image": {
          "description": "the image file (JPEG, PNG, WebP, HEIC, …; max 40 MB)"
         },
         "preview": {
          "description": "0 to omit the preview thumbnail"
         }
        }
       }
      }
     }
    }
   }
  },
  "/analyze/video": {
   "post": {
    "summary": "Analyse one video (max 200 MB).",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "analysis_basis": {
             "description": "Video: which evidence decided the verdict — frames, face, talking_head, temporal, audio, provenance, conflict or preview."
            },
            "audio": {
             "description": "Video: the soundtrack screen's result (advisory), or null when the clip has no audio or the screen is off."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "face_coverage": {
             "description": "Video: share of sampled frames with a face large enough to score."
            },
            "face_p_ai": {
             "description": "Video: the face-swap model's own score (0-1), present only when it read a face (coverage >= 0.25) and did not fail; evidence only, it never decides the verdict alone."
            },
            "frame_scan": {
             "description": "Video: the per-frame scan — n frames, median score, share confidently AI, the thresholds, a timeline of {t, p_ai, band} and the composition in percent."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "scope": {
             "description": "Video: whole-frame or face scope of the scan."
            },
            "t2v": {
             "description": "Video: the text-to-video head's read, or null."
            },
            "talking_head": {
             "description": "Video: the talking-head head's read, or null."
            },
            "timings_ms": {
             "description": "Video: time spent per stage."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "content"
          },
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "provenance_kind": {
             "description": "Provenance replies: which marker was found (e.g. C2PA digitalSourceType)."
            },
            "provenance_verified": {
             "description": "Provenance replies: true when the C2PA signature was cryptographically verified."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "provenance"
          }
         ]
        }
       }
      }
     },
     "503": {
      "description": "Modality under maintenance (status: under_maintenance), the daily capacity is reached, the server is busy, or a model is still warming up after idle. Retry later.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "message": {
           "description": "Human-readable message (maintenance replies)."
          },
          "modality": {
           "description": "Which modality is under maintenance."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          },
          "status": {
           "description": "Health: ok, or degraded when any entry in checks.failed is set (HTTP stays 200). Maintenance replies: under_maintenance."
          },
          "verdict": {
           "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
          }
         },
         "title": "under_maintenance"
        }
       }
      }
     }
    },
    "security": [
     {
      "apiKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "properties": {
         "video": {
          "description": "the video file (MP4, MOV, WebM, …)"
         }
        }
       }
      }
     }
    }
   }
  },
  "/analyze/url": {
   "post": {
    "summary": "Fetch a public link (image, video or social post) and analyse its media.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "decided_by": {
             "description": "Which head produced p_ai and the verdict: the primary image model, the web-copy sidecar, a fallback candidate, or provenance."
            },
            "degradation": {
             "description": "Image: how much forensic signal the file has left — level (none, moderate, heavy), reasons (e.g. recompressed, resized, no camera EXIF), size and format."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "heatmap_token": {
             "description": "Image: pass back to POST /heatmap within 30 minutes to get the region map without re-sending the verdict."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "preview": {
             "description": "Image: a small (<= 360 px) JPEG data URL of the analysed image; omitted when the request sends preview=0."
            },
            "source_media": {
             "description": "Link check: original (the full file was fetched) or preview (only a social preview image was available)."
            },
            "source_url": {
             "description": "Link check: the URL that was fetched."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "image"
          },
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "decided_by": {
             "description": "Which head produced p_ai and the verdict: the primary image model, the web-copy sidecar, a fallback candidate, or provenance."
            },
            "degradation": {
             "description": "Image: how much forensic signal the file has left — level (none, moderate, heavy), reasons (e.g. recompressed, resized, no camera EXIF), size and format."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "heatmap_token": {
             "description": "Image: pass back to POST /heatmap within 30 minutes to get the region map without re-sending the verdict."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "localized_edit": {
             "description": "Image judged real: the edited-photo screen's result {score, thr, fires}; fires=true turns the verdict into 'Uncertain — possible local edit'."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "preview": {
             "description": "Image: a small (<= 360 px) JPEG data URL of the analysed image; omitted when the request sends preview=0."
            },
            "source_media": {
             "description": "Link check: original (the full file was fetched) or preview (only a social preview image was available)."
            },
            "source_url": {
             "description": "Link check: the URL that was fetched."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "image+edit_screen"
          },
          {
           "type": "object",
           "properties": {
            "ai_probability": {
             "description": "Same value as p_ai (kept for older clients)."
            },
            "analysis_basis": {
             "description": "Video: which evidence decided the verdict — frames, face, talking_head, temporal, audio, provenance, conflict or preview."
            },
            "audio": {
             "description": "Video: the soundtrack screen's result (advisory), or null when the clip has no audio or the screen is off."
            },
            "build": {
             "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
            },
            "conclusion": {
             "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
            },
            "confidence": {
             "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
            },
            "config_version": {
             "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
            },
            "cover_p_ai": {
             "description": "Link check of a video: the image model's score on the post's cover picture. Recorded, never a verdict on its own."
            },
            "detail": {
             "description": "One human-readable sentence about how the verdict was reached, or null."
            },
            "elapsed_ms": {
             "description": "Server-side processing time in milliseconds (excludes upload and network)."
            },
            "face_coverage": {
             "description": "Video: share of sampled frames with a face large enough to score."
            },
            "face_p_ai": {
             "description": "Video: the face-swap model's own score (0-1), present only when it read a face (coverage >= 0.25) and did not fail; evidence only, it never decides the verdict alone."
            },
            "frame_scan": {
             "description": "Video: the per-frame scan — n frames, median score, share confidently AI, the thresholds, a timeline of {t, p_ai, band} and the composition in percent."
            },
            "label": {
             "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "mix": {
             "description": "Present only on some uncertain replies: 'Mixed signals' — ai_share and real_share (each model's read on its own calibrated band), every model's read, and the parts (time segments or an image region). The verdict stays uncertain."
            },
            "model": {
             "description": "Name of the head that produced the score."
            },
            "p_ai": {
             "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
            },
            "scope": {
             "description": "Video: whole-frame or face scope of the scan."
            },
            "source_media": {
             "description": "Link check: original (the full file was fetched) or preview (only a social preview image was available)."
            },
            "source_url": {
             "description": "Link check: the URL that was fetched."
            },
            "t2v": {
             "description": "Video: the text-to-video head's read, or null."
            },
            "talking_head": {
             "description": "Video: the talking-head head's read, or null."
            },
            "timings_ms": {
             "description": "Video: time spent per stage."
            },
            "verdict": {
             "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
            }
           },
           "title": "video+cover"
          }
         ]
        }
       }
      }
     },
     "422": {
      "description": "The link is not a fetchable http(s) URL, or the platform refused the fetch.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "bad_url"
        }
       }
      }
     },
     "503": {
      "description": "Modality under maintenance (status: under_maintenance), the daily capacity is reached, the server is busy, or a model is still warming up after idle. Retry later.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "message": {
           "description": "Human-readable message (maintenance replies)."
          },
          "modality": {
           "description": "Which modality is under maintenance."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          },
          "status": {
           "description": "Health: ok, or degraded when any entry in checks.failed is set (HTTP stays 200). Maintenance replies: under_maintenance."
          },
          "verdict": {
           "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
          }
         },
         "title": "video_maintenance"
        }
       }
      }
     }
    },
    "security": [
     {
      "apiKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "url": {
          "description": "http(s) URL of the post or file"
         }
        }
       }
      }
     }
    }
   }
  },
  "/heatmap": {
   "post": {
    "summary": "Region map for an image already analysed; not billed when nothing is painted.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "type": "object",
           "properties": {
            "bbox": {
             "description": "Heat map: the localizer region [x0, y0, x1, y1] in pixels of the returned heat-map image (not the original upload)."
            },
            "bbox_frame": {
             "description": "Heat map / localized edit: the coordinate frame of bbox — 'image' (full-image pixel coordinates) or 'square' (the centre square the localizer examined)."
            },
            "edited_score": {
             "description": "Heat map: the localizer's score for the region it found, 0-1."
            },
            "examined": {
             "description": "Heat map / localized edit: the region the localizer actually looked at (only the centre is examined), in the same frame as bbox."
            },
            "flag": {
             "description": "Heat map: 'check' when a region is worth a human look (amber, never an accusation)."
            },
            "gate": {
             "description": "Heat map: the verdict score the heat map was gated on. Audio: the content gate that applied."
            },
            "heatmap": {
             "description": "Heat map: a PNG data URL, or null when there is nothing to paint."
            },
            "mask_area": {
             "description": "Heat map: share of the image inside the localizer's mask."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "zoom": {
             "description": "Heat map: a PNG data URL zoomed on the region, or null."
            }
           },
           "title": "inpaint_localizer"
          },
          {
           "type": "object",
           "properties": {
            "gate": {
             "description": "Heat map: the verdict score the heat map was gated on. Audio: the content gate that applied."
            },
            "heatmap": {
             "description": "Heat map: a PNG data URL, or null when there is nothing to paint."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            }
           },
           "title": "aeroblade"
          },
          {
           "type": "object",
           "properties": {
            "gate": {
             "description": "Heat map: the verdict score the heat map was gated on. Audio: the content gate that applied."
            },
            "heatmap": {
             "description": "Heat map: a PNG data URL, or null when there is nothing to paint."
            },
            "method": {
             "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
            },
            "reason": {
             "description": "Why no heat map was painted, or why a token was refused."
            }
           },
           "title": "none"
          }
         ]
        }
       }
      }
     },
     "410": {
      "description": "heatmap_token expired (30 minutes) — call /analyze again.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "heatmap": {
           "description": "Heat map: a PNG data URL, or null when there is nothing to paint."
          },
          "method": {
           "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
          },
          "reason": {
           "description": "Why no heat map was painted, or why a token was refused."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "token_expired"
        }
       }
      }
     }
    },
    "security": [
     {
      "apiKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "properties": {
         "image": {
          "description": "the same image"
         },
         "token": {
          "description": "heatmap_token from /analyze"
         },
         "gate": {
          "description": "the verdict's p_ai"
         }
        }
       }
      }
     }
    }
   }
  },
  "/auth/signup": {
   "post": {
    "summary": "Create a free API key (100 checks, 30 requests/min).",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "api_key": {
           "description": "The new API key. Shown once; only its hash is stored."
          },
          "note": {
           "description": "Key signup: save-once reminder."
          },
          "ok": {
           "description": "true on success."
          },
          "plan": {
           "description": "Key plan (free)."
          },
          "quota": {
           "description": "Key signup: number of checks included."
          },
          "rate_per_min": {
           "description": "Key signup: requests per minute allowed."
          },
          "upgrade": {
           "description": "Key signup: what happens when the quota runs out."
          },
          "usage": {
           "description": "Key signup: a ready-to-run curl example with the base URL."
          }
         },
         "title": "default"
        }
       }
      }
     },
     "400": {
      "description": "Bad request: no file in the expected field, empty upload, undecodable media, bad JSON, or a sealed request with a stale timestamp (server_time tells the right clock).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "bad_email"
        }
       }
      }
     }
    },
    "security": [],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "email": {
          "description": "your e-mail address"
         }
        }
       }
      }
     }
    }
   }
  },
  "/analyze/audio": {
   "post": {
    "summary": "Analyse one audio file (first 120 s). Advisory while the audio head has no measured operating point.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "advisory": {
           "description": "Audio without an accepted calibration only: the research score (score, the statistic it is, the release gate the head failed, the window aggregates and per-window reads). Not a verdict; p_ai and ai_probability are null in these replies."
          },
          "advisory_only": {
           "description": "true when the audio head has no measured operating point: the score is information, never a verdict."
          },
          "agg": {
           "description": "Audio: which window aggregate the verdict statistic uses (pooled, w_max, w_med or w_q75)."
          },
          "ai_probability": {
           "description": "Same value as p_ai (kept for older clients)."
          },
          "analyzed_s": {
           "description": "Audio: seconds actually analysed (only the first 120 s of a file are read)."
          },
          "build": {
           "description": "Which deploy answered: the first 12 characters of the git commit (plus '-dirty' when uncommitted served edits shipped), or 'unknown'. On /health, the full record: sha, dirty, diff_sha256, branch, built_at, artifacts (sha256[:12] of each served model and config) and id."
          },
          "calibration": {
           "description": "Audio: where the thresholds come from; 'UNCALIBRATED' means the reply is advisory."
          },
          "conclusion": {
           "description": "Legacy summary string: 'AI-Generated', 'REAL' or 'UNCERTAIN'. Prefer verdict."
          },
          "confidence": {
           "description": "0-100: how far the score sits past the threshold that decided the verdict. 0 on uncertain replies. Not a probability."
          },
          "config_version": {
           "description": "Short hash of the served operating configuration (heads and thresholds). It changes whenever a verdict could change for the same file; log it with each result."
          },
          "detail": {
           "description": "One human-readable sentence about how the verdict was reached, or null."
          },
          "domain": {
           "description": "Audio: the kind of sound the head judged (speech, music or env)."
          },
          "domain_p": {
           "description": "Audio: the head's confidence in that domain, 0-1."
          },
          "duration_s": {
           "description": "Audio: seconds analysed (at most 120)."
          },
          "elapsed_ms": {
           "description": "Server-side processing time in milliseconds (excludes upload and network)."
          },
          "file_s": {
           "description": "Audio: the whole file's length in seconds, when it could be measured; else null."
          },
          "gate": {
           "description": "Heat map: the verdict score the heat map was gated on. Audio: the content gate that applied."
          },
          "kind": {
           "description": "Audio: which head family scored the file (speccube or xlsr)."
          },
          "label": {
           "description": "Short display text: 'Likely AI-generated', 'Likely authentic', 'Uncertain — flag for review', 'Mixed signals', 'Uncertain — possible local edit' and their video or audio variants."
          },
          "method": {
           "description": "content (pixels were scored), provenance (signed AI provenance decided it) or video_content; on /heatmap: inpaint_localizer, aeroblade or none."
          },
          "model": {
           "description": "Name of the head that produced the score."
          },
          "n_win": {
           "description": "Audio: number of 8-second windows scored."
          },
          "offmanifold_z": {
           "description": "Audio: distance from the real-sound manifold, in standard deviations."
          },
          "p_ai": {
           "description": "The model's score in [0, 1]. Higher means more AI-like. It is a ranking score compared with thresholds, not a calibrated probability; read verdict first."
          },
          "speech_frac": {
           "description": "Audio: share of the file detected as speech."
          },
          "truncated": {
           "description": "Audio: true when the file was longer than the analysed part (truncation_note then says how much was read)."
          },
          "verdict": {
           "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
          },
          "views": {
           "description": "Audio: the spectrogram views the head reads."
          }
         },
         "title": "advisory"
        }
       }
      }
     },
     "400": {
      "description": "Bad request: no file in the expected field, empty upload, undecodable media, bad JSON, or a sealed request with a stale timestamp (server_time tells the right clock).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "no_file"
        }
       }
      }
     },
     "415": {
      "description": "Unsupported file type.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "unsupported_type"
        }
       }
      }
     },
     "503": {
      "description": "Modality under maintenance (status: under_maintenance), the daily capacity is reached, the server is busy, or a model is still warming up after idle. Retry later.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "message": {
           "description": "Human-readable message (maintenance replies)."
          },
          "modality": {
           "description": "Which modality is under maintenance."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          },
          "status": {
           "description": "Health: ok, or degraded when any entry in checks.failed is set (HTTP stays 200). Maintenance replies: under_maintenance."
          },
          "verdict": {
           "description": "ai, real or uncertain (maintenance replies: unavailable). Uncertain is deliberate: the evidence did not support an answer."
          }
         },
         "title": "under_maintenance"
        }
       }
      }
     }
    },
    "security": [
     {
      "apiKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "properties": {
         "audio": {
          "description": "wav, mp3, m4a, aac, ogg, opus, flac or webm"
         }
        }
       }
      }
     }
    }
   }
  },
  "/auth/register": {
   "post": {
    "summary": "Create an account and sign in.",
    "responses": {
     "201": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "account": {
           "description": "The signed-in profile: e-mail, name, company, verification state, its API keys (prefix, plan, quota used and total, last use — never the key) and, on GET /account, its sessions."
          },
          "email_sender": {
           "description": "Whether e-mail actions work: {configured, support}. When configured is false, password reset is unavailable; write to support."
          },
          "expires_at": {
           "description": "Session expiry, Unix time (30 days)."
          },
          "ok": {
           "description": "true on success."
          },
          "token": {
           "description": "Session bearer token. Send as 'Authorization: Bearer <token>' on /account routes. Only its hash is stored."
          }
         },
         "title": "default"
        }
       }
      }
     },
     "409": {
      "description": "Sealed request made to a rotated key (reseal with the returned pubkey), or the account / key limit already exists.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "exists"
        }
       }
      }
     }
    },
    "security": [],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "email": {
          "description": "e-mail"
         },
         "password": {
          "description": "at least 10 characters"
         },
         "name": {
          "description": "optional"
         },
         "company": {
          "description": "optional"
         }
        }
       }
      }
     }
    }
   }
  },
  "/auth/login": {
   "post": {
    "summary": "Sign in; returns a session token.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "account": {
           "description": "The signed-in profile: e-mail, name, company, verification state, its API keys (prefix, plan, quota used and total, last use — never the key) and, on GET /account, its sessions."
          },
          "email_sender": {
           "description": "Whether e-mail actions work: {configured, support}. When configured is false, password reset is unavailable; write to support."
          },
          "expires_at": {
           "description": "Session expiry, Unix time (30 days)."
          },
          "ok": {
           "description": "true on success."
          },
          "token": {
           "description": "Session bearer token. Send as 'Authorization: Bearer <token>' on /account routes. Only its hash is stored."
          }
         },
         "title": "default"
        }
       }
      }
     },
     "401": {
      "description": "Missing API key (send X-API-Key), expired site token, or not signed in (/account).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "wrong"
        }
       }
      }
     }
    },
    "security": [],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "email": {
          "description": "e-mail"
         },
         "password": {
          "description": "password"
         }
        }
       }
      }
     }
    }
   }
  },
  "/account": {
   "get": {
    "summary": "The signed-in profile, keys, sessions and 30-day usage.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "account": {
           "description": "The signed-in profile: e-mail, name, company, verification state, its API keys (prefix, plan, quota used and total, last use — never the key) and, on GET /account, its sessions."
          },
          "email_sender": {
           "description": "Whether e-mail actions work: {configured, support}. When configured is false, password reset is unavailable; write to support."
          },
          "limits": {
           "description": "Account: max keys, free quota and free rate."
          },
          "ok": {
           "description": "true on success."
          },
          "usage_30d": {
           "description": "Account: calls in the last 30 days — total, by route, days active."
          }
         },
         "title": "default"
        }
       }
      }
     },
     "401": {
      "description": "Missing API key (send X-API-Key), expired site token, or not signed in (/account).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "description": "Errors: a stable machine-readable code (e.g. too_large, rate_limited, image_unavailable, timeout, token_expired). Branch on this, not on the error sentence."
          },
          "error": {
           "description": "Machine-readable or human-readable error string."
          },
          "request_id": {
           "description": "Errors: the id of this request, also sent as the X-Request-Id header on every reply. Quote it to support."
          },
          "retryable": {
           "description": "Errors: true when the same request may succeed later (429, 5xx); honour Retry-After."
          }
         },
         "title": "signed_out"
        }
       }
      }
     }
    },
    "security": [
     {
      "bearer": []
     }
    ]
   }
  },
  "/auth/logout": {
   "post": {
    "summary": "End this session.",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "ok": {
           "description": "true on success."
          }
         },
         "title": "default"
        }
       }
      }
     }
    },
    "security": [
     {
      "bearer": []
     }
    ]
   }
  }
 }
}
