{
  "openapi": "3.1.0",
  "info": {
    "title": "Perspicax API",
    "version": "1.0.0",
    "summary": "Detect fully AI-generated, photo-like still images.",
    "description": "Detect fully AI-generated, photo-like still images with TrueEye 2.3. Send an image as a file or a link and get a three-band verdict (`ai_generated`, `inconclusive` or `not_ai_generated`), a score, the provenance found in the file and the model version.\n\n**Authentication.** Send `Authorization: Bearer <key>`. Keys are made in the Perspicax app (Developers > API keys).\n\n- `pcx_test_…`: development mode, on every plan. It answers only for images your workspace has already scanned, with their stored result, and uses no quota. See https://perspicax.io/docs/api#development-mode.\n- `pcx_live_…`: production mode, on Pro and Enterprise. Every image is checked and uses your quota.\n\n**Errors** are RFC 9457 problem details with a stable `code`: https://perspicax.io/docs/api#errors.\n\nImages: up to 20 MB, JPEG, PNG, WebP, HEIC, HEIF, AVIF, GIF (still). Full documentation, a playground and a Postman collection: https://perspicax.io/docs/api.",
    "termsOfService": "https://perspicax.io/legal/terms",
    "contact": {
      "name": "Perspicax",
      "email": "contact@perspicax.io",
      "url": "https://perspicax.io/docs/api"
    }
  },
  "servers": [
    {
      "url": "https://perspicax.io",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Scans",
      "description": "Send an image and read its verdict."
    },
    {
      "name": "Sharing",
      "description": "Public result pages for scans."
    },
    {
      "name": "Usage",
      "description": "Your workspace’s quota."
    },
    {
      "name": "Models",
      "description": "The detector models you can ask for."
    }
  ],
  "externalDocs": {
    "description": "Perspicax API docs",
    "url": "https://perspicax.io/docs/api"
  },
  "paths": {
    "/api/v1/scans": {
      "post": {
        "operationId": "createScan",
        "summary": "Create a scan",
        "description": "Send one image, as a file (`multipart/form-data`, field `image`) or as a link (JSON `{ \"url\": … }`). A production key checks it with TrueEye and uses your quota; a development key answers at once with the stored result of an image your workspace already scanned, and uses none. In production, add `Prefer: wait=N` to get the verdict in the same call, and `share=true` for a public result page. An `Idempotency-Key` makes retries safe in both modes.\n\nDocs: https://perspicax.io/docs/api#create-scan",
        "tags": [
          "Scans"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Prefer"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/RequestId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/CreateScanFile"
              },
              "encoding": {
                "image": {
                  "contentType": "image/jpeg, image/png, image/webp, image/heic, image/heif, image/avif, image/gif"
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateScanUrl"
              },
              "example": {
                "url": "https://perspicax.io/images/harbour.jpg"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The scan is done: in production within your `Prefer: wait`, and always in development mode (a stored result).",
            "headers": {
              "Preference-Applied": {
                "$ref": "#/components/headers/Preference-Applied"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scan"
                },
                "examples": {
                  "production": {
                    "summary": "Production key: a completed scan",
                    "value": {
                      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "status": "completed",
                      "source": "api",
                      "mode": "live",
                      "replayOf": null,
                      "model": {
                        "id": "trueeye-2.3",
                        "name": "TrueEye",
                        "version": "2.3"
                      },
                      "createdAt": "2026-10-28T09:12:47.210Z",
                      "completedAt": "2026-10-28T09:12:49.350Z",
                      "image": {
                        "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                        "width": 2400,
                        "height": 1018,
                        "mime": "image/jpeg",
                        "bytes": 299028,
                        "filename": "harbour.jpg",
                        "previewUrl": "https://perspicax.io/api/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                      },
                      "result": {
                        "label": "ai_generated",
                        "inconclusiveReason": null,
                        "labelSource": "detector",
                        "coarse": "high",
                        "score": 0.9731,
                        "thresholds": null,
                        "provenance": {
                          "c2pa": null,
                          "iptcDigitalSourceType": null
                        },
                        "metadata": {
                          "exifPresent": false,
                          "cameraMake": null,
                          "software": null
                        },
                        "detector": {
                          "model": "TrueEye",
                          "version": "2.3",
                          "cropScores": null,
                          "photoLike": true,
                          "mock": false
                        },
                        "autoCrop": {
                          "enabled": true,
                          "applied": false,
                          "box": null,
                          "imageWidth": 2400,
                          "imageHeight": 1018
                        }
                      },
                      "usage": {
                        "counted": true,
                        "cached": false
                      },
                      "latencyMs": 2140,
                      "share": null
                    }
                  },
                  "development": {
                    "summary": "Development key: the stored result of an earlier scan",
                    "value": {
                      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "status": "completed",
                      "source": "api",
                      "mode": "test",
                      "replayOf": "0b8e4f27-3c1d-4a6e-9f52-7e1a3d9c4b88",
                      "model": {
                        "id": "trueeye-2.3",
                        "name": "TrueEye",
                        "version": "2.3"
                      },
                      "createdAt": "2026-10-28T09:12:47.210Z",
                      "completedAt": "2026-10-28T09:12:47.222Z",
                      "image": {
                        "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                        "width": 2400,
                        "height": 1018,
                        "mime": "image/jpeg",
                        "bytes": 299028,
                        "filename": "harbour.jpg",
                        "previewUrl": null
                      },
                      "result": {
                        "label": "ai_generated",
                        "inconclusiveReason": null,
                        "labelSource": "detector",
                        "coarse": "high",
                        "score": 0.9731,
                        "thresholds": null,
                        "provenance": {
                          "c2pa": null,
                          "iptcDigitalSourceType": null
                        },
                        "metadata": {
                          "exifPresent": false,
                          "cameraMake": null,
                          "software": null
                        },
                        "detector": {
                          "model": "TrueEye",
                          "version": "2.3",
                          "cropScores": null,
                          "photoLike": true,
                          "mock": false
                        },
                        "autoCrop": {
                          "enabled": true,
                          "applied": false,
                          "box": null,
                          "imageWidth": 2400,
                          "imageHeight": 1018
                        }
                      },
                      "usage": {
                        "counted": false,
                        "cached": true
                      },
                      "latencyMs": 12,
                      "share": null
                    }
                  },
                  "shared": {
                    "summary": "With share=true: the public result page",
                    "value": {
                      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "status": "completed",
                      "source": "api",
                      "mode": "live",
                      "replayOf": null,
                      "model": {
                        "id": "trueeye-2.3",
                        "name": "TrueEye",
                        "version": "2.3"
                      },
                      "createdAt": "2026-10-28T09:12:47.210Z",
                      "completedAt": "2026-10-28T09:12:49.350Z",
                      "image": {
                        "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                        "width": 2400,
                        "height": 1018,
                        "mime": "image/jpeg",
                        "bytes": 299028,
                        "filename": "harbour.jpg",
                        "previewUrl": "https://perspicax.io/api/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                      },
                      "result": {
                        "label": "ai_generated",
                        "inconclusiveReason": null,
                        "labelSource": "detector",
                        "coarse": "high",
                        "score": 0.9731,
                        "thresholds": null,
                        "provenance": {
                          "c2pa": null,
                          "iptcDigitalSourceType": null
                        },
                        "metadata": {
                          "exifPresent": false,
                          "cameraMake": null,
                          "software": null
                        },
                        "detector": {
                          "model": "TrueEye",
                          "version": "2.3",
                          "cropScores": null,
                          "photoLike": true,
                          "mock": false
                        },
                        "autoCrop": {
                          "enabled": true,
                          "applied": false,
                          "box": null,
                          "imageWidth": 2400,
                          "imageHeight": 1018
                        }
                      },
                      "usage": {
                        "counted": true,
                        "cached": false
                      },
                      "latencyMs": 2140,
                      "share": {
                        "url": "https://perspicax.io/r/Xq7Lm2Pz9Rt4Vb6N"
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted and still running: poll `Location` (every `Retry-After` seconds) until `status` is `completed` or `failed`.",
            "headers": {
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scan"
                },
                "examples": {
                  "queued": {
                    "summary": "Still running",
                    "value": {
                      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "status": "queued",
                      "source": "api",
                      "mode": "live",
                      "replayOf": null,
                      "model": {
                        "id": "trueeye-2.3",
                        "name": "TrueEye",
                        "version": "2.3"
                      },
                      "createdAt": "2026-10-28T09:12:47.210Z",
                      "completedAt": null,
                      "image": {
                        "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                        "width": 2400,
                        "height": 1018,
                        "mime": "image/jpeg",
                        "bytes": 299028,
                        "filename": "harbour.jpg",
                        "previewUrl": "https://perspicax.io/api/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                      },
                      "result": null,
                      "usage": {
                        "counted": false,
                        "cached": false
                      },
                      "latencyMs": null,
                      "share": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is not valid.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "summary": "validation_failed",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "Send an image file (multipart field \"image\") or JSON { \"url\": \"https://…\" }.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Production mode is not on this plan, or the quota is used up.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  },
                  "quota_exceeded": {
                    "summary": "quota_exceeded",
                    "value": {
                      "type": "https://perspicax.io/problems/quota_exceeded",
                      "title": "Scan limit reached",
                      "status": 402,
                      "detail": "You've used today's 35 scans. They reset at 00:00 UTC.",
                      "code": "quota_exceeded",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "quota": {
                        "plan": "pro",
                        "limit": 35,
                        "used": 35,
                        "remaining": 0,
                        "window": "day",
                        "resetsAt": "2026-10-29T00:00:00.000Z",
                        "needsSignup": false,
                        "needsUpgrade": false,
                        "overage": null,
                        "blocked": null
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io. Or the member who created the key may not scan, or (development mode) may not see the workspace’s scans.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The first request with this Idempotency-Key is still creating its scan.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "Location": {
                "$ref": "#/components/headers/Location"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "summary": "validation_failed",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 409,
                      "detail": "A request with this Idempotency-Key is still in progress. Retry shortly.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 1
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "The image is over the size limit.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "file_too_large": {
                    "summary": "file_too_large",
                    "value": {
                      "type": "https://perspicax.io/problems/file_too_large",
                      "title": "File too large",
                      "status": 413,
                      "detail": "Images can be at most 20 MB.",
                      "code": "file_too_large",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "415": {
            "description": "Not a supported still image.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unsupported_media_type": {
                    "summary": "unsupported_media_type",
                    "value": {
                      "type": "https://perspicax.io/problems/unsupported_media_type",
                      "title": "Unsupported media type",
                      "status": 415,
                      "detail": "Only JPEG, PNG, WebP, HEIC/HEIF, AVIF and GIF images are supported.",
                      "code": "unsupported_media_type",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Development mode does not know the image, the image can’t be read, a field is invalid, or the Idempotency-Key was reused.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unknown_test_image": {
                    "summary": "unknown_test_image",
                    "value": {
                      "type": "https://perspicax.io/problems/unknown_test_image",
                      "title": "Unknown image in development mode",
                      "status": 422,
                      "detail": "Development mode answers for images you have already scanned on perspicax.io. Scan it in the app first, or use a production key (Pro).",
                      "code": "unknown_test_image",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "docs": "https://perspicax.io/docs/api#development-mode",
                      "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596"
                    }
                  },
                  "unprocessable_image": {
                    "summary": "unprocessable_image",
                    "value": {
                      "type": "https://perspicax.io/problems/unprocessable_image",
                      "title": "The image could not be read",
                      "status": 422,
                      "detail": "The image is smaller than 16 pixels on a side.",
                      "code": "unprocessable_image",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  },
                  "validation_failed": {
                    "summary": "validation_failed",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 422,
                      "detail": "Unknown model \"trueeye-9\". Use one of: trueeye-2.3.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "errors": [
                        {
                          "path": "model",
                          "message": "Allowed: trueeye-2.3"
                        }
                      ]
                    }
                  },
                  "idempotency_key_reused": {
                    "summary": "idempotency_key_reused",
                    "value": {
                      "type": "https://perspicax.io/problems/idempotency_key_reused",
                      "title": "Idempotency key reused with a different request",
                      "status": 422,
                      "detail": "This Idempotency-Key was already used with a different request body.",
                      "code": "idempotency_key_reused",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key, or over a development-mode limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "The key’s rate limit",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  },
                  "development": {
                    "summary": "A development-mode limit",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Development mode allows 30 requests a minute per key. Try again in 17 s.",
                      "code": "rate_limited",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17,
                      "docs": "https://perspicax.io/docs/api#development-mode"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Something went wrong on our side; nothing was counted.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "internal": {
                    "summary": "internal",
                    "value": {
                      "type": "https://perspicax.io/problems/internal",
                      "title": "Internal error",
                      "status": 500,
                      "detail": "The scan failed. It was not counted; try again.",
                      "code": "internal",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The detector is briefly unavailable; nothing was counted.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "detector_unavailable": {
                    "summary": "detector_unavailable",
                    "value": {
                      "type": "https://perspicax.io/problems/detector_unavailable",
                      "title": "The detector is unavailable",
                      "status": 503,
                      "detail": "The detector is temporarily unavailable. Try again shortly.",
                      "code": "detector_unavailable",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 30
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl · Create a scan from a file",
            "source": "curl https://perspicax.io/api/v1/scans \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\" \\\n  -H \"Prefer: wait=30\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -F \"image=@harbour.jpg\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript · Create a scan from a file",
            "source": "// Node.js 18 or later (save as scan.mjs)\nimport { readFile } from 'node:fs/promises';\nimport { randomUUID } from 'node:crypto';\n\nconst form = new FormData();\nform.append('image', new Blob([await readFile('harbour.jpg')]), 'harbour.jpg');\n\nconst res = await fetch('https://perspicax.io/api/v1/scans', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n    Prefer: 'wait=30',\n    'Idempotency-Key': randomUUID(),\n  },\n  body: form,\n});\nconst scan = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${scan.code}: ${scan.detail}`);\nconsole.log(scan.status, scan.result?.label, scan.result?.score);"
          },
          {
            "lang": "Python",
            "label": "Python · Create a scan from a file",
            "source": "# pip install requests\nimport os\nimport uuid\n\nimport requests\n\nwith open(\"harbour.jpg\", \"rb\") as image:\n    res = requests.post(\n        \"https://perspicax.io/api/v1/scans\",\n        headers={\n            \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n            \"Prefer\": \"wait=30\",\n            \"Idempotency-Key\": str(uuid.uuid4()),\n        },\n        files={\"image\": image},\n        timeout=60,\n    )\n\nscan = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {scan['code']}: {scan.get('detail')}\")\nresult = scan[\"result\"] or {}\nprint(scan[\"status\"], result.get(\"label\"), result.get(\"score\"))"
          },
          {
            "lang": "PHP",
            "label": "PHP · Create a scan from a file",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/scans');\ncurl_setopt_array($ch, [\n    CURLOPT_POST => true,\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 60,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n        'Prefer: wait=30',\n        'Idempotency-Key: ' . bin2hex(random_bytes(16)),\n    ],\n    CURLOPT_POSTFIELDS => [\n        'image' => new CURLFile('harbour.jpg'),\n    ],\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$scan = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$scan['code']}: {$scan['detail']}\" . PHP_EOL);\n    exit(1);\n}\necho $scan['status'], ' ', $scan['result']['label'] ?? '-', ' ', $scan['result']['score'] ?? '-', PHP_EOL;"
          },
          {
            "lang": "Go",
            "label": "Go · Create a scan from a file",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\tvar body bytes.Buffer\n\tform := multipart.NewWriter(&body)\n\tpart, err := form.CreateFormFile(\"image\", \"harbour.jpg\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfile, err := os.Open(\"harbour.jpg\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer file.Close()\n\tif _, err := io.Copy(part, file); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tform.Close()\n\n\treq, err := http.NewRequest(http.MethodPost, \"https://perspicax.io/api/v1/scans\", &body)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\treq.Header.Set(\"Content-Type\", form.FormDataContentType())\n\treq.Header.Set(\"Prefer\", \"wait=30\")\n\treq.Header.Set(\"Idempotency-Key\", newKey())\n\n\tclient := &http.Client{Timeout: 60 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar scan map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&scan); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, scan[\"code\"], scan[\"detail\"])\n\t}\n\tresult, _ := scan[\"result\"].(map[string]any)\n\tfmt.Println(scan[\"status\"], result[\"label\"], result[\"score\"])\n}\n\n// newKey returns a random Idempotency-Key.\nfunc newKey() string {\n\tb := make([]byte, 16)\n\tif _, err := rand.Read(b); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treturn hex.EncodeToString(b)\n}"
          },
          {
            "lang": "Shell",
            "label": "curl · Create a scan from a URL",
            "source": "curl https://perspicax.io/api/v1/scans \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Prefer: wait=30\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"url\":\"https://perspicax.io/images/harbour.jpg\"}'"
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript · Create a scan from a URL",
            "source": "// Node.js 18 or later (save as scan.mjs)\nimport { randomUUID } from 'node:crypto';\n\nconst res = await fetch('https://perspicax.io/api/v1/scans', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n    'Content-Type': 'application/json',\n    Prefer: 'wait=30',\n    'Idempotency-Key': randomUUID(),\n  },\n  body: JSON.stringify({ url: 'https://perspicax.io/images/harbour.jpg' }),\n});\nconst scan = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${scan.code}: ${scan.detail}`);\nconsole.log(scan.status, scan.result?.label, scan.result?.score);"
          },
          {
            "lang": "Python",
            "label": "Python · Create a scan from a URL",
            "source": "# pip install requests\nimport os\nimport uuid\n\nimport requests\n\nres = requests.post(\n    \"https://perspicax.io/api/v1/scans\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n        \"Prefer\": \"wait=30\",\n        \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"url\": \"https://perspicax.io/images/harbour.jpg\"},\n    timeout=60,\n)\n\nscan = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {scan['code']}: {scan.get('detail')}\")\nresult = scan[\"result\"] or {}\nprint(scan[\"status\"], result.get(\"label\"), result.get(\"score\"))"
          },
          {
            "lang": "PHP",
            "label": "PHP · Create a scan from a URL",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/scans');\ncurl_setopt_array($ch, [\n    CURLOPT_POST => true,\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 60,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n        'Content-Type: application/json',\n        'Prefer: wait=30',\n        'Idempotency-Key: ' . bin2hex(random_bytes(16)),\n    ],\n    CURLOPT_POSTFIELDS => json_encode(['url' => 'https://perspicax.io/images/harbour.jpg']),\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$scan = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$scan['code']}: {$scan['detail']}\" . PHP_EOL);\n    exit(1);\n}\necho $scan['status'], ' ', $scan['result']['label'] ?? '-', ' ', $scan['result']['score'] ?? '-', PHP_EOL;"
          },
          {
            "lang": "Go",
            "label": "Go · Create a scan from a URL",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\tpayload, err := json.Marshal(map[string]any{\"url\": \"https://perspicax.io/images/harbour.jpg\"})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq, err := http.NewRequest(http.MethodPost, \"https://perspicax.io/api/v1/scans\", bytes.NewReader(payload))\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Prefer\", \"wait=30\")\n\treq.Header.Set(\"Idempotency-Key\", newKey())\n\n\tclient := &http.Client{Timeout: 60 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar scan map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&scan); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, scan[\"code\"], scan[\"detail\"])\n\t}\n\tresult, _ := scan[\"result\"].(map[string]any)\n\tfmt.Println(scan[\"status\"], result[\"label\"], result[\"score\"])\n}\n\n// newKey returns a random Idempotency-Key.\nfunc newKey() string {\n\tb := make([]byte, 16)\n\tif _, err := rand.Read(b); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treturn hex.EncodeToString(b)\n}"
          }
        ]
      },
      "get": {
        "operationId": "listScans",
        "summary": "List scans",
        "description": "The workspace’s scans, newest first, one page at a time: follow `nextCursor` until it is `null`. A development key lists development-mode answers only; a production key lists every other scan of the workspace (the app, the website and production keys).\n\nDocs: https://perspicax.io/docs/api#list-scans",
        "tags": [
          "Scans"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/StatusFilter"
          },
          {
            "$ref": "#/components/parameters/LabelFilter"
          },
          {
            "$ref": "#/components/parameters/SourceFilter"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of scans.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanList"
                },
                "examples": {
                  "page": {
                    "summary": "A page of scans",
                    "value": {
                      "items": [
                        {
                          "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                          "status": "completed",
                          "source": "api",
                          "mode": "live",
                          "replayOf": null,
                          "model": {
                            "id": "trueeye-2.3",
                            "name": "TrueEye",
                            "version": "2.3"
                          },
                          "createdAt": "2026-10-28T09:12:47.210Z",
                          "completedAt": "2026-10-28T09:12:49.350Z",
                          "image": {
                            "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                            "width": 2400,
                            "height": 1018,
                            "mime": "image/jpeg",
                            "bytes": 299028,
                            "filename": "harbour.jpg",
                            "previewUrl": "https://perspicax.io/api/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                          },
                          "result": {
                            "label": "ai_generated",
                            "inconclusiveReason": null,
                            "labelSource": "detector",
                            "coarse": "high",
                            "score": 0.9731,
                            "thresholds": null,
                            "provenance": {
                              "c2pa": null,
                              "iptcDigitalSourceType": null
                            },
                            "metadata": {
                              "exifPresent": false,
                              "cameraMake": null,
                              "software": null
                            },
                            "detector": {
                              "model": "TrueEye",
                              "version": "2.3",
                              "cropScores": null,
                              "photoLike": true,
                              "mock": false
                            },
                            "autoCrop": {
                              "enabled": true,
                              "applied": false,
                              "box": null,
                              "imageWidth": 2400,
                              "imageHeight": 1018
                            }
                          },
                          "usage": {
                            "counted": true,
                            "cached": false
                          },
                          "latencyMs": 2140,
                          "share": null
                        },
                        {
                          "id": "0b8e4f27-3c1d-4a6e-9f52-7e1a3d9c4b88",
                          "status": "completed",
                          "source": "api",
                          "mode": "live",
                          "replayOf": null,
                          "model": {
                            "id": "trueeye-2.3",
                            "name": "TrueEye",
                            "version": "2.3"
                          },
                          "createdAt": "2026-10-28T09:12:47.210Z",
                          "completedAt": "2026-10-28T09:12:49.080Z",
                          "image": {
                            "sha256": "a828aec688f64ca7fb9fb51418eccfd658d4592ef8ed8370d519086b2eb68762",
                            "width": 928,
                            "height": 1152,
                            "mime": "image/jpeg",
                            "bytes": 201286,
                            "filename": "kebab.jpg",
                            "previewUrl": "https://perspicax.io/api/scans/0b8e4f27-3c1d-4a6e-9f52-7e1a3d9c4b88/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                          },
                          "result": {
                            "label": "not_ai_generated",
                            "inconclusiveReason": null,
                            "labelSource": "detector",
                            "coarse": "low",
                            "score": 0.0318,
                            "thresholds": null,
                            "provenance": {
                              "c2pa": null,
                              "iptcDigitalSourceType": null
                            },
                            "metadata": {
                              "exifPresent": false,
                              "cameraMake": null,
                              "software": null
                            },
                            "detector": {
                              "model": "TrueEye",
                              "version": "2.3",
                              "cropScores": null,
                              "photoLike": true,
                              "mock": false
                            },
                            "autoCrop": {
                              "enabled": true,
                              "applied": false,
                              "box": null,
                              "imageWidth": 928,
                              "imageHeight": 1152
                            }
                          },
                          "usage": {
                            "counted": true,
                            "cached": false
                          },
                          "latencyMs": 1870,
                          "share": null
                        }
                      ],
                      "nextCursor": "MjAyNi0xMC0yOFQwODo1NTowMy4xMThafDBiOGU0ZjI3LTNjMWQtNGE2ZS05ZjUyLTdlMWEzZDljNGI4OA"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A query parameter or the cursor is not valid.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "summary": "validation_failed",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "Send an image file (multipart field \"image\") or JSON { \"url\": \"https://…\" }.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "A production key of a workspace without production mode (Free).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io. Or the member who created the key may not see the workspace’s scans.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "rate_limited",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/scans",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl \"https://perspicax.io/api/v1/scans?limit=20\" \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript",
            "source": "// Node.js 18 or later (save as scan.mjs)\n\nconst res = await fetch('https://perspicax.io/api/v1/scans?limit=20', {\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n  },\n});\nconst page = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${page.code}: ${page.detail}`);\nfor (const scan of page.items) console.log(scan.id, scan.status, scan.result?.label);\nconsole.log('Next page:', page.nextCursor);"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "# pip install requests\nimport os\n\nimport requests\n\nres = requests.get(\n    \"https://perspicax.io/api/v1/scans?limit=20\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n    },\n    timeout=30,\n)\n\npage = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {page['code']}: {page.get('detail')}\")\nfor scan in page[\"items\"]:\n    print(scan[\"id\"], scan[\"status\"], (scan[\"result\"] or {}).get(\"label\"))\nprint(\"Next page:\", page[\"nextCursor\"])"
          },
          {
            "lang": "PHP",
            "label": "PHP",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/scans?limit=20');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 30,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n    ],\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$page = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$page['code']}: {$page['detail']}\" . PHP_EOL);\n    exit(1);\n}\nforeach ($page['items'] as $scan) {\n    echo $scan['id'], ' ', $scan['status'], ' ', $scan['result']['label'] ?? '-', PHP_EOL;\n}\necho 'Next page: ', $page['nextCursor'] ?? 'none', PHP_EOL;"
          },
          {
            "lang": "Go",
            "label": "Go",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\treq, err := http.NewRequest(http.MethodGet, \"https://perspicax.io/api/v1/scans?limit=20\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar page map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&page); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, page[\"code\"], page[\"detail\"])\n\t}\n\titems, _ := page[\"items\"].([]any)\n\tfor _, item := range items {\n\t\tscan, _ := item.(map[string]any)\n\t\tfmt.Println(scan[\"id\"], scan[\"status\"])\n\t}\n\tfmt.Println(\"Next page:\", page[\"nextCursor\"])\n}"
          }
        ]
      }
    },
    "/api/v1/scans/{id}": {
      "get": {
        "operationId": "getScan",
        "summary": "Retrieve a scan",
        "description": "One scan of your workspace, in any state: check `status`. This is the URL a `202` points to in `Location`. A failed scan has `result: null` and was not counted. A development key reads development-mode answers only.\n\nDocs: https://perspicax.io/docs/api#get-scan",
        "tags": [
          "Scans"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ScanId"
          }
        ],
        "responses": {
          "200": {
            "description": "The scan.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scan"
                },
                "examples": {
                  "completed": {
                    "summary": "A completed scan",
                    "value": {
                      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "status": "completed",
                      "source": "api",
                      "mode": "live",
                      "replayOf": null,
                      "model": {
                        "id": "trueeye-2.3",
                        "name": "TrueEye",
                        "version": "2.3"
                      },
                      "createdAt": "2026-10-28T09:12:47.210Z",
                      "completedAt": "2026-10-28T09:12:49.350Z",
                      "image": {
                        "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                        "width": 2400,
                        "height": 1018,
                        "mime": "image/jpeg",
                        "bytes": 299028,
                        "filename": "harbour.jpg",
                        "previewUrl": "https://perspicax.io/api/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                      },
                      "result": {
                        "label": "ai_generated",
                        "inconclusiveReason": null,
                        "labelSource": "detector",
                        "coarse": "high",
                        "score": 0.9731,
                        "thresholds": null,
                        "provenance": {
                          "c2pa": null,
                          "iptcDigitalSourceType": null
                        },
                        "metadata": {
                          "exifPresent": false,
                          "cameraMake": null,
                          "software": null
                        },
                        "detector": {
                          "model": "TrueEye",
                          "version": "2.3",
                          "cropScores": null,
                          "photoLike": true,
                          "mock": false
                        },
                        "autoCrop": {
                          "enabled": true,
                          "applied": false,
                          "box": null,
                          "imageWidth": 2400,
                          "imageHeight": 1018
                        }
                      },
                      "usage": {
                        "counted": true,
                        "cached": false
                      },
                      "latencyMs": 2140,
                      "share": null
                    }
                  },
                  "running": {
                    "summary": "Still running",
                    "value": {
                      "id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "status": "running",
                      "source": "api",
                      "mode": "live",
                      "replayOf": null,
                      "model": {
                        "id": "trueeye-2.3",
                        "name": "TrueEye",
                        "version": "2.3"
                      },
                      "createdAt": "2026-10-28T09:12:47.210Z",
                      "completedAt": null,
                      "image": {
                        "sha256": "f750730fb99d9aaf619583559e175ec3069aceb2b10fc3d2565148ce28b6b596",
                        "width": 2400,
                        "height": 1018,
                        "mime": "image/jpeg",
                        "bytes": 299028,
                        "filename": "harbour.jpg",
                        "previewUrl": "https://perspicax.io/api/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/image?exp=1761643067&sig=Q2xlYXJseUlsbHVzdHJhdGl2ZQ"
                      },
                      "result": null,
                      "usage": {
                        "counted": false,
                        "cached": false
                      },
                      "latencyMs": null,
                      "share": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a scan id (a UUID).",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "summary": "Not a scan id",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "Please check the id.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans/not-a-uuid",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "A production key of a workspace without production mode (Free).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such scan in your workspace, or not one this key reads: another mode’s, or one the key’s creator may not see.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "not_found": {
                    "summary": "not_found",
                    "value": {
                      "type": "https://perspicax.io/problems/not_found",
                      "title": "Not found",
                      "status": 404,
                      "detail": "Scan not found.",
                      "code": "not_found",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "rate_limited",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript",
            "source": "// Node.js 18 or later (save as scan.mjs)\n\nconst res = await fetch('https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f', {\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n  },\n});\nconst scan = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${scan.code}: ${scan.detail}`);\nconsole.log(scan.status, scan.result?.label, scan.result?.score);"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "# pip install requests\nimport os\n\nimport requests\n\nres = requests.get(\n    \"https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n    },\n    timeout=30,\n)\n\nscan = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {scan['code']}: {scan.get('detail')}\")\nresult = scan[\"result\"] or {}\nprint(scan[\"status\"], result.get(\"label\"), result.get(\"score\"))"
          },
          {
            "lang": "PHP",
            "label": "PHP",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 30,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n    ],\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$scan = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$scan['code']}: {$scan['detail']}\" . PHP_EOL);\n    exit(1);\n}\necho $scan['status'], ' ', $scan['result']['label'] ?? '-', ' ', $scan['result']['score'] ?? '-', PHP_EOL;"
          },
          {
            "lang": "Go",
            "label": "Go",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\treq, err := http.NewRequest(http.MethodGet, \"https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar scan map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&scan); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, scan[\"code\"], scan[\"detail\"])\n\t}\n\tresult, _ := scan[\"result\"].(map[string]any)\n\tfmt.Println(scan[\"status\"], result[\"label\"], result[\"score\"])\n}"
          }
        ]
      }
    },
    "/api/v1/scans/{id}/share": {
      "post": {
        "operationId": "shareScan",
        "summary": "Share a scan",
        "description": "Makes the scan’s public result page, or returns the one it already has: each scan has one link. Anyone with the link sees the photo and the verdict, never the score, the file name or who shared it. A scan that failed and a development-mode answer cannot be shared.\n\nDocs: https://perspicax.io/docs/api#share-scan",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ScanId"
          }
        ],
        "responses": {
          "200": {
            "description": "The scan was already shared: the same link.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanShare"
                },
                "examples": {
                  "share": {
                    "summary": "The public page",
                    "value": {
                      "url": "https://perspicax.io/r/Xq7Lm2Pz9Rt4Vb6N"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The page was made now.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanShare"
                },
                "examples": {
                  "share": {
                    "summary": "The public page",
                    "value": {
                      "url": "https://perspicax.io/r/Xq7Lm2Pz9Rt4Vb6N"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The scan failed, or it is a development-mode answer: neither has a public page. Or the id is not a scan id (a UUID).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "failed": {
                    "summary": "A scan that failed",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "This scan failed, so it has no result to share.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  },
                  "development": {
                    "summary": "A development-mode answer",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "This is a development-mode answer: it repeats an earlier result and has no public page. Share the original scan, or use a live key.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  },
                  "malformed": {
                    "summary": "Not a scan id",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "Please check the id.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans/not-a-uuid/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "A production key of a workspace without production mode (Free).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such scan in your workspace, or not one this key reads: another mode’s, or one the key’s creator may not see.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "not_found": {
                    "summary": "not_found",
                    "value": {
                      "type": "https://perspicax.io/problems/not_found",
                      "title": "Not found",
                      "status": 404,
                      "detail": "Scan not found.",
                      "code": "not_found",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "rate_limited",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X POST https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript",
            "source": "// Node.js 18 or later (save as scan.mjs)\n\nconst res = await fetch('https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share', {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n  },\n});\nconst share = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${share.code}: ${share.detail}`);\nconsole.log(share.url);"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "# pip install requests\nimport os\n\nimport requests\n\nres = requests.post(\n    \"https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n    },\n    timeout=30,\n)\n\nshare = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {share['code']}: {share.get('detail')}\")\nprint(share[\"url\"])"
          },
          {
            "lang": "PHP",
            "label": "PHP",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share');\ncurl_setopt_array($ch, [\n    CURLOPT_POST => true,\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 30,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n    ],\n    CURLOPT_POSTFIELDS => '',\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$share = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$share['code']}: {$share['detail']}\" . PHP_EOL);\n    exit(1);\n}\necho $share['url'], PHP_EOL;"
          },
          {
            "lang": "Go",
            "label": "Go",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\treq, err := http.NewRequest(http.MethodPost, \"https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar share map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&share); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, share[\"code\"], share[\"detail\"])\n\t}\n\tfmt.Println(share[\"url\"])\n}"
          }
        ]
      },
      "delete": {
        "operationId": "unshareScan",
        "summary": "Stop sharing a scan",
        "description": "The public link answers `404` from now on. Also fine when the scan was not shared.\n\nDocs: https://perspicax.io/docs/api#stop-sharing",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ScanId"
          }
        ],
        "responses": {
          "200": {
            "description": "Not shared any more.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "examples": {
                  "ok": {
                    "summary": "Done",
                    "value": {
                      "ok": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a scan id (a UUID).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "validation_failed": {
                    "summary": "Not a scan id",
                    "value": {
                      "type": "https://perspicax.io/problems/validation_failed",
                      "title": "Invalid request",
                      "status": 400,
                      "detail": "Please check the id.",
                      "code": "validation_failed",
                      "instance": "/api/v1/scans/not-a-uuid/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "A production key of a workspace without production mode (Free).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such scan in your workspace, or not one this key reads: another mode’s, or one the key’s creator may not see.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "not_found": {
                    "summary": "not_found",
                    "value": {
                      "type": "https://perspicax.io/problems/not_found",
                      "title": "Not found",
                      "status": 404,
                      "detail": "Scan not found.",
                      "code": "not_found",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "rate_limited",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl -X DELETE https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript",
            "source": "// Node.js 18 or later (save as scan.mjs)\n\nconst res = await fetch('https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share', {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${data.code}: ${data.detail}`);\nconsole.log(data);"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "# pip install requests\nimport os\n\nimport requests\n\nres = requests.delete(\n    \"https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n    },\n    timeout=30,\n)\n\ndata = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {data['code']}: {data.get('detail')}\")\nprint(data)"
          },
          {
            "lang": "PHP",
            "label": "PHP",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share');\ncurl_setopt_array($ch, [\n    CURLOPT_CUSTOMREQUEST => 'DELETE',\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 30,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n    ],\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$data = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$data['code']}: {$data['detail']}\" . PHP_EOL);\n    exit(1);\n}\nprint_r($data);"
          },
          {
            "lang": "Go",
            "label": "Go",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\treq, err := http.NewRequest(http.MethodDelete, \"https://perspicax.io/api/v1/scans/6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f/share\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar data map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&data); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, data[\"code\"], data[\"detail\"])\n\t}\n\tout, _ := json.MarshalIndent(data, \"\", \"  \")\n\tfmt.Println(string(out))\n}"
          }
        ]
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Get usage",
        "description": "The workspace’s quota, the numbers the app shows: Free’s and Pro’s scans of the UTC day (Pro’s are shared with the app), or the Enterprise pool of the contract period. Any key can read it, in either mode, with the `X-Quota-*` headers. Development mode never uses this quota, and reading it uses none.\n\nDocs: https://perspicax.io/docs/api#usage",
        "tags": [
          "Usage"
        ],
        "responses": {
          "200": {
            "description": "The workspace’s quota.",
            "headers": {
              "X-Quota-Limit": {
                "$ref": "#/components/headers/X-Quota-Limit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/X-Quota-Remaining"
              },
              "X-Quota-Reset": {
                "$ref": "#/components/headers/X-Quota-Reset"
              },
              "X-Quota-Overage-Used": {
                "$ref": "#/components/headers/X-Quota-Overage-Used"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quota"
                },
                "examples": {
                  "pro": {
                    "summary": "A Pro workspace",
                    "value": {
                      "plan": "pro",
                      "limit": 35,
                      "used": 12,
                      "remaining": 23,
                      "window": "day",
                      "resetsAt": "2026-10-29T00:00:00.000Z",
                      "needsSignup": false,
                      "needsUpgrade": false,
                      "overage": null,
                      "blocked": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/usage",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "A production key of a workspace without production mode (Free).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/usage",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/usage",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "rate_limited",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/usage",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://perspicax.io/api/v1/usage \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript",
            "source": "// Node.js 18 or later (save as scan.mjs)\n\nconst res = await fetch('https://perspicax.io/api/v1/usage', {\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${data.code}: ${data.detail}`);\nconsole.log(data);"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "# pip install requests\nimport os\n\nimport requests\n\nres = requests.get(\n    \"https://perspicax.io/api/v1/usage\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n    },\n    timeout=30,\n)\n\ndata = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {data['code']}: {data.get('detail')}\")\nprint(data)"
          },
          {
            "lang": "PHP",
            "label": "PHP",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/usage');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 30,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n    ],\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$data = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$data['code']}: {$data['detail']}\" . PHP_EOL);\n    exit(1);\n}\nprint_r($data);"
          },
          {
            "lang": "Go",
            "label": "Go",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\treq, err := http.NewRequest(http.MethodGet, \"https://perspicax.io/api/v1/usage\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar data map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&data); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, data[\"code\"], data[\"detail\"])\n\t}\n\tout, _ := json.MarshalIndent(data, \"\", \"  \")\n\tfmt.Println(string(out))\n}"
          }
        ]
      }
    },
    "/api/v1/models": {
      "get": {
        "operationId": "listModels",
        "summary": "List models",
        "description": "The detector models a scan can ask for with `model`. Without `model` you get the default, `trueeye-2.3`. A good first call to check your key.\n\nDocs: https://perspicax.io/docs/api#models",
        "tags": [
          "Models"
        ],
        "responses": {
          "200": {
            "description": "The models.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelList"
                },
                "examples": {
                  "models": {
                    "summary": "Models",
                    "value": {
                      "items": [
                        {
                          "id": "trueeye-2.3",
                          "name": "TrueEye 2.3",
                          "description": "Detects fully AI-generated, photo-like still images.",
                          "default": true
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "The key is missing, malformed, unknown or revoked, or the member who created it has left the workspace.",
            "headers": {
              "WWW-Authenticate": {
                "$ref": "#/components/headers/WWW-Authenticate"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "unauthorized": {
                    "summary": "unauthorized",
                    "value": {
                      "type": "https://perspicax.io/problems/unauthorized",
                      "title": "Sign-in required",
                      "status": 401,
                      "detail": "A valid API key is required: Authorization: Bearer pcx_live_… or pcx_test_…",
                      "code": "unauthorized",
                      "instance": "/api/v1/models",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "A production key of a workspace without production mode (Free).",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "upgrade_required": {
                    "summary": "upgrade_required",
                    "value": {
                      "type": "https://perspicax.io/problems/upgrade_required",
                      "title": "Upgrade required",
                      "status": 402,
                      "detail": "Live keys use production mode, which comes with Pro and Enterprise. Upgrade to Pro, or use a test key (pcx_test_): development mode is free on every plan.",
                      "code": "upgrade_required",
                      "instance": "/api/v1/models",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "suggestedPlan": "pro",
                      "docs": "https://perspicax.io/docs/api#production-mode"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "The API is switched off for this workspace, or the call bypassed https://perspicax.io.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "forbidden": {
                    "summary": "forbidden",
                    "value": {
                      "type": "https://perspicax.io/problems/forbidden",
                      "title": "Forbidden",
                      "status": 403,
                      "detail": "The API is switched off for this workspace. Write to contact@perspicax.io to switch it back on.",
                      "code": "forbidden",
                      "instance": "/api/v1/models",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too many requests with this key.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "rate_limited",
                    "value": {
                      "type": "https://perspicax.io/problems/rate_limited",
                      "title": "Too many requests",
                      "status": 429,
                      "detail": "Rate limit of 60 requests per minute exceeded.",
                      "code": "rate_limited",
                      "instance": "/api/v1/models",
                      "requestId": "b3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "retryAfter": 17
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "label": "curl",
            "source": "curl https://perspicax.io/api/v1/models \\\n  -H \"Authorization: Bearer $PERSPICAX_API_KEY\""
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript",
            "source": "// Node.js 18 or later (save as scan.mjs)\n\nconst res = await fetch('https://perspicax.io/api/v1/models', {\n  headers: {\n    Authorization: `Bearer ${process.env.PERSPICAX_API_KEY}`,\n  },\n});\nconst data = await res.json();\nif (!res.ok) throw new Error(`${res.status} ${data.code}: ${data.detail}`);\nconsole.log(data);"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "# pip install requests\nimport os\n\nimport requests\n\nres = requests.get(\n    \"https://perspicax.io/api/v1/models\",\n    headers={\n        \"Authorization\": f\"Bearer {os.environ['PERSPICAX_API_KEY']}\",\n    },\n    timeout=30,\n)\n\ndata = res.json()\nif not res.ok:\n    raise SystemExit(f\"{res.status_code} {data['code']}: {data.get('detail')}\")\nprint(data)"
          },
          {
            "lang": "PHP",
            "label": "PHP",
            "source": "<?php\n// PHP 8 with the curl extension\n$ch = curl_init('https://perspicax.io/api/v1/models');\ncurl_setopt_array($ch, [\n    CURLOPT_RETURNTRANSFER => true,\n    CURLOPT_TIMEOUT => 30,\n    CURLOPT_HTTPHEADER => [\n        'Authorization: Bearer ' . getenv('PERSPICAX_API_KEY'),\n    ],\n]);\n$raw = curl_exec($ch);\nif ($raw === false) {\n    fwrite(STDERR, 'Request failed: ' . curl_error($ch) . PHP_EOL);\n    exit(1);\n}\n$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);\n$data = json_decode($raw, true);\n\nif ($status >= 400) {\n    fwrite(STDERR, \"{$status} {$data['code']}: {$data['detail']}\" . PHP_EOL);\n    exit(1);\n}\nprint_r($data);"
          },
          {
            "lang": "Go",
            "label": "Go",
            "source": "// go run scan.go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\treq, err := http.NewRequest(http.MethodGet, \"https://perspicax.io/api/v1/models\", nil)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+os.Getenv(\"PERSPICAX_API_KEY\"))\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer res.Body.Close()\n\n\tvar data map[string]any\n\tif err := json.NewDecoder(res.Body).Decode(&data); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tif res.StatusCode >= 400 {\n\t\tlog.Fatalf(\"%d %v: %v\", res.StatusCode, data[\"code\"], data[\"detail\"])\n\t}\n\tout, _ := json.MarshalIndent(data, \"\", \"  \")\n\tfmt.Println(string(out))\n}"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key: pcx_test_… (development) or pcx_live_… (production), followed by 32 letters and digits."
      }
    },
    "schemas": {
      "Scan": {
        "type": "object",
        "description": "One image and its verdict. Every scan endpoint answers with this object.",
        "required": [
          "id",
          "status",
          "source",
          "mode",
          "replayOf",
          "model",
          "createdAt",
          "completedAt",
          "image",
          "result",
          "usage",
          "latencyMs",
          "share"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The scan’s id. Read it again with `GET /api/v1/scans/{id}`."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "completed",
              "failed"
            ],
            "description": "`queued` and `running` while the check runs, then `completed` (with `result`) or `failed` (not counted: send the image again with a new Idempotency-Key)."
          },
          "source": {
            "type": "string",
            "enum": [
              "landing",
              "app",
              "api"
            ],
            "description": "Where the scan was made: `landing` (the website), `app`, or `api` (API keys and the Developers playground)."
          },
          "mode": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ],
            "description": "`test`: a development-mode answer (an earlier result repeated). `live`: a real check (the app, the website, production keys)."
          },
          "replayOf": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Development mode: the id of the earlier scan whose stored result this answer repeats. `null` in production mode."
          },
          "model": {
            "type": "object",
            "description": "The detector that answered.",
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "The model id, what you send as `model`: `trueeye-2.3` by default."
              },
              "name": {
                "const": "TrueEye",
                "description": "Always `TrueEye`."
              },
              "version": {
                "type": "string",
                "description": "The model version, such as `2.3`."
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the scan was created (ISO 8601, UTC)."
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the scan finished; `null` until then."
          },
          "image": {
            "type": "object",
            "description": "The file we received.",
            "required": [
              "sha256",
              "width",
              "height",
              "mime",
              "bytes",
              "filename",
              "previewUrl"
            ],
            "properties": {
              "sha256": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$",
                "description": "SHA-256 of the exact bytes received, in hex. Development mode recognises images by this hash."
              },
              "width": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Width as displayed (EXIF orientation applied), in pixels; `null` when it could not be read."
              },
              "height": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Height as displayed, in pixels; `null` when it could not be read."
              },
              "mime": {
                "type": "string",
                "enum": [
                  "image/jpeg",
                  "image/png",
                  "image/webp",
                  "image/heic",
                  "image/heif",
                  "image/avif",
                  "image/gif"
                ],
                "description": "The format, from the file’s bytes (not its name)."
              },
              "bytes": {
                "type": "integer",
                "description": "File size in bytes."
              },
              "filename": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The file name you sent (cleaned), or the last part of the URL."
              },
              "previewUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "A link, valid for 5 minutes, to a display copy: WebP, at most 2048 px on the longest side, every piece of metadata removed. Never the original. `null` in development mode and when no copy can be made."
              }
            }
          },
          "result": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ScanResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "The verdict. `null` until the scan is `completed`."
          },
          "usage": {
            "type": "object",
            "description": "What this scan cost.",
            "required": [
              "counted",
              "cached"
            ],
            "properties": {
              "counted": {
                "type": "boolean",
                "description": "`true` when the scan used your quota: a completed production scan. `false` in development mode, for failed scans and for Idempotency-Key retries."
              },
              "cached": {
                "type": "boolean",
                "description": "`true` when nothing was checked for this answer: a development-mode answer, or a retry with the same Idempotency-Key."
              }
            }
          },
          "latencyMs": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Time from upload to verdict, in milliseconds; `null` until the scan finishes."
          },
          "share": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ScanShare"
              },
              {
                "type": "null"
              }
            ],
            "description": "The scan’s public result page, when it is shared (`share=true`, or `POST /api/v1/scans/{id}/share`); `null` otherwise. Always `null` on a development-mode answer."
          }
        }
      },
      "ScanResult": {
        "type": "object",
        "description": "The verdict and what we read from the file. `null` until the scan is `completed`.",
        "required": [
          "label",
          "inconclusiveReason",
          "labelSource",
          "coarse",
          "score",
          "thresholds",
          "provenance",
          "metadata",
          "detector",
          "autoCrop"
        ],
        "properties": {
          "label": {
            "type": "string",
            "enum": [
              "ai_generated",
              "inconclusive",
              "not_ai_generated"
            ],
            "description": "The verdict: `ai_generated`, `inconclusive` or `not_ai_generated`. Build your logic on this field."
          },
          "inconclusiveReason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "between_thresholds",
              "outside_scope",
              null
            ],
            "description": "`between_thresholds` when the label is `inconclusive` because the result sits between a clear AI and a clear real answer; `null` otherwise. (`outside_scope` appears only on scans made before 28 October 2026.)"
          },
          "labelSource": {
            "type": "string",
            "enum": [
              "detector",
              "content_credentials"
            ],
            "description": "`detector` when the image check decided the label; `content_credentials` when a verified manifest from Google or OpenAI declares the image AI-generated. `score` is filled in either way."
          },
          "coarse": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "low",
              "middle",
              "high",
              null
            ],
            "description": "The band that goes with the label: `low` (likely real), `middle` (inconclusive) or `high` (likely AI). `null` only on old outside-scope scans."
          },
          "score": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "From 0 to 1, rounded to 4 decimals: higher means stronger signs of AI generation. It is a score, not a probability, and the cut-offs between the bands are not published (they can change with the model), so base decisions on `label`. The exact score comes with production access: in development mode on Free it is `null` (the label and `coarse` band are there)."
          },
          "thresholds": {
            "type": "null",
            "description": "Reserved. Always `null` in API answers."
          },
          "provenance": {
            "type": "object",
            "description": "Provenance found in the file, read before the image check.",
            "required": [
              "c2pa",
              "iptcDigitalSourceType"
            ],
            "properties": {
              "c2pa": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ContentCredentials"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The Content Credentials manifest, or `null` when the file has none."
              },
              "iptcDigitalSourceType": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The IPTC digital source type from the file’s metadata, or `null`."
              }
            }
          },
          "metadata": {
            "type": "object",
            "description": "Camera metadata found in the file.",
            "required": [
              "exifPresent",
              "cameraMake",
              "software"
            ],
            "properties": {
              "exifPresent": {
                "type": "boolean",
                "description": "`true` when the file has EXIF data."
              },
              "cameraMake": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The camera maker from EXIF, or `null`."
              },
              "software": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The software from EXIF, or `null`."
              }
            }
          },
          "detector": {
            "type": "object",
            "description": "The model that answered, by its public name.",
            "required": [
              "model",
              "version",
              "cropScores",
              "photoLike",
              "mock"
            ],
            "properties": {
              "model": {
                "const": "TrueEye",
                "description": "Always `TrueEye`."
              },
              "version": {
                "type": "string",
                "description": "The model version, such as `2.3`."
              },
              "cropScores": {
                "type": "null",
                "description": "Reserved. Always `null` in API answers."
              },
              "photoLike": {
                "type": "boolean",
                "description": "`false` when the image looks like a screenshot, poster, document or artwork rather than a photo. It still gets a verdict; treat it as a weaker signal, because the model is built for photos."
              },
              "mock": {
                "type": "boolean",
                "description": "Always `false` on perspicax.io. `true` only on test servers that use a stand-in detector."
              }
            }
          },
          "autoCrop": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AutoCrop"
              },
              {
                "type": "null"
              }
            ],
            "description": "What auto-crop did; `null` on scans made before auto-crop existed."
          }
        }
      },
      "AutoCrop": {
        "type": "object",
        "description": "What auto-crop did. Borders and app UI around the photo (for example a social post screenshot: status bar, header, caption, buttons) are left out, so only the photo is checked. It does not catch every screenshot: a dark photo in a dark app, for example, may stay uncropped.",
        "required": [
          "enabled",
          "applied",
          "box",
          "imageWidth",
          "imageHeight"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "What you asked for: `autoCrop`, true unless you sent false."
          },
          "applied": {
            "type": "boolean",
            "description": "`true` when borders or app UI were found and left out."
          },
          "box": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "x",
                  "y",
                  "width",
                  "height"
                ],
                "properties": {
                  "x": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Left edge of the checked region, in pixels."
                  },
                  "y": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Top edge of the checked region, in pixels."
                  },
                  "width": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Width of the checked region, in pixels."
                  },
                  "height": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Height of the checked region, in pixels."
                  }
                }
              },
              {
                "type": "null"
              }
            ],
            "description": "The region that was kept and checked, in pixels of the image as displayed (EXIF orientation applied); `null` when nothing was left out."
          },
          "imageWidth": {
            "type": "integer",
            "description": "Width of the whole image as displayed, in pixels."
          },
          "imageHeight": {
            "type": "integer",
            "description": "Height of the whole image as displayed, in pixels."
          }
        }
      },
      "ContentCredentials": {
        "type": "object",
        "description": "A summary of the file’s Content Credentials (C2PA) manifest.",
        "required": [
          "present",
          "claimGenerator",
          "issuer",
          "actions",
          "digitalSourceType"
        ],
        "properties": {
          "present": {
            "const": true,
            "description": "Always `true` when the object is there."
          },
          "claimGenerator": {
            "type": [
              "string",
              "null"
            ],
            "description": "The software that wrote the manifest."
          },
          "issuer": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who signed the manifest."
          },
          "actions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The actions the manifest records, such as `c2pa.created`."
          },
          "digitalSourceType": {
            "type": [
              "string",
              "null"
            ],
            "description": "The IPTC digital source type the manifest declares."
          }
        }
      },
      "ScanShare": {
        "type": "object",
        "description": "A public result page: anyone with the link sees the photo and the verdict, never the score, the file name or who shared it.",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The page, `https://perspicax.io/r/<slug>`. Its result card is `<url>/card.png` and its printable report `<url>/report.pdf`."
          }
        }
      },
      "ScanList": {
        "type": "object",
        "description": "A page of scans, newest first.",
        "required": [
          "items",
          "nextCursor"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Scan"
            },
            "description": "The scans on this page."
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Send it as `cursor` to get the next page; `null` on the last page."
          }
        }
      },
      "Model": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "default"
        ],
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "trueeye-2.3"
            ],
            "description": "What you send as `model`; scans return it as `model.id`."
          },
          "name": {
            "type": "string",
            "description": "The display name."
          },
          "description": {
            "type": "string",
            "description": "What the model is for, in one line."
          },
          "default": {
            "type": "boolean",
            "description": "`true` for the model used when a request names none."
          }
        }
      },
      "ModelList": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Model"
            },
            "description": "The models a scan can ask for. Exactly one is the default."
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "An error, as RFC 9457 problem details (`application/problem+json`). Branch on `code`, which never changes; `detail` is a sentence for people.",
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A link to this code’s documentation: `https://perspicax.io/problems/<code>`."
          },
          "title": {
            "type": "string",
            "description": "A short, fixed summary of the code."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status, repeated."
          },
          "code": {
            "type": "string",
            "enum": [
              "validation_failed",
              "unauthorized",
              "upgrade_required",
              "quota_exceeded",
              "forbidden",
              "not_found",
              "file_too_large",
              "unsupported_media_type",
              "unknown_test_image",
              "unprocessable_image",
              "idempotency_key_reused",
              "rate_limited",
              "internal",
              "detector_unavailable"
            ],
            "description": "The stable error code. See the errors reference."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong in this case, in one sentence."
          },
          "instance": {
            "type": "string",
            "description": "The path you called."
          },
          "requestId": {
            "type": "string",
            "description": "This request’s id, also sent as `X-Request-Id`. Quote it to support."
          },
          "errors": {
            "type": "array",
            "description": "`validation_failed`: the fields at fault.",
            "items": {
              "type": "object",
              "required": [
                "path",
                "message"
              ],
              "properties": {
                "path": {
                  "type": "string",
                  "description": "The field, such as `model`."
                },
                "message": {
                  "type": "string",
                  "description": "What is wrong with it."
                }
              }
            }
          },
          "retryAfter": {
            "type": "integer",
            "description": "Seconds to wait before retrying (also the `Retry-After` header)."
          },
          "quota": {
            "$ref": "#/components/schemas/Quota",
            "description": "`quota_exceeded`: where the workspace stands (`blocked` says why when a contract stops scans)."
          },
          "suggestedPlan": {
            "type": "string",
            "enum": [
              "pro"
            ],
            "description": "`upgrade_required`: the plan that allows the request, `pro`."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "A link to the docs about this problem: development mode for `unknown_test_image` and development mode’s `rate_limited`, production mode for `upgrade_required`."
          },
          "sha256": {
            "type": "string",
            "description": "`unknown_test_image`: the SHA-256 of the image you sent."
          }
        }
      },
      "Quota": {
        "type": "object",
        "description": "The workspace’s quota: the numbers the app shows. Production scans use it (the app’s scans too); development mode never does. `GET /api/v1/usage` answers it, and `quota_exceeded` carries it.",
        "required": [
          "plan",
          "limit",
          "used",
          "remaining",
          "window",
          "resetsAt",
          "needsSignup",
          "needsUpgrade",
          "overage",
          "blocked"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro",
              "enterprise"
            ],
            "description": "The workspace’s plan."
          },
          "limit": {
            "type": "integer",
            "minimum": 0,
            "description": "The scans in the period: Free and Pro, the UTC day’s; Enterprise, the pool of the contract period."
          },
          "used": {
            "type": "integer",
            "minimum": 0,
            "description": "The scans used in the period."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "The scans left in the period."
          },
          "window": {
            "type": "string",
            "enum": [
              "day",
              "week",
              "month",
              "year"
            ],
            "description": "The period: `day` on Free and Pro (a UTC day); on Enterprise, the contract’s `day`, `week`, `month` or `year`."
          },
          "resetsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the period restarts (ISO 8601, UTC): the next 00:00 UTC, or the end of the contract period."
          },
          "needsSignup": {
            "type": "boolean",
            "description": "Always `false` for a workspace (it is for signed-out visitors of the website)."
          },
          "needsUpgrade": {
            "type": "boolean",
            "description": "`true` on Free when the day’s scans are used up: Pro would allow more."
          },
          "overage": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/QuotaOverage"
              },
              {
                "type": "null"
              }
            ],
            "description": "Enterprise only: scans past the pool, as set in the organisation’s contract; `null` otherwise."
          },
          "blocked": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "setting_up",
              "no_contract",
              "not_started",
              "paused",
              "ended",
              null
            ],
            "description": "Enterprise only: why no scan can run whatever the numbers say (the organisation is being set up, no contract, not started yet, paused or ended); `null` when scans may run."
          }
        }
      },
      "QuotaOverage": {
        "type": "object",
        "description": "Enterprise only: scans past the pool, as set in the organisation’s contract.",
        "required": [
          "enabled",
          "used",
          "cap",
          "pricePerScan",
          "amountDue"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "`true` when the organisation’s contract allows scans past the pool."
          },
          "used": {
            "type": "integer",
            "minimum": 0,
            "description": "Scans past the pool in this period (a count)."
          },
          "cap": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The most scans past the pool the contract allows in a period; `null` for no cap."
          },
          "pricePerScan": {
            "type": [
              "number",
              "null"
            ],
            "description": "Always `null` in API answers: the price is in the organisation’s contract, and members who manage billing see it in the app."
          },
          "amountDue": {
            "type": [
              "number",
              "null"
            ],
            "description": "Always `null` in API answers: what the overage comes to is shown in the app to members who manage billing."
          }
        }
      },
      "CreateScanFile": {
        "type": "object",
        "required": [
          "image"
        ],
        "properties": {
          "image": {
            "type": "string",
            "format": "binary",
            "contentMediaType": "application/octet-stream",
            "description": "The image file, at most 20 MB: JPEG, PNG, WebP, HEIC, HEIF, AVIF, GIF (still)."
          },
          "model": {
            "type": "string",
            "default": "trueeye-2.3",
            "description": "A model id from `GET /api/v1/models`; `trueeye-2.3` when absent. An unknown id is `422 validation_failed` naming the allowed ids."
          },
          "autoCrop": {
            "type": "boolean",
            "default": true,
            "description": "Borders and app UI around the photo (for example a social post screenshot) are left out, and only the photo is checked; `result.autoCrop.box` says what was kept. `false` checks the whole image as sent. `auto_crop` is accepted too. In a form, send `true` or `false`."
          },
          "share": {
            "type": "boolean",
            "default": false,
            "description": "Production mode: `true` also makes the scan’s public result page, also on a `202`; its link comes back as `share.url`. Development mode accepts it and makes no page: its answers repeat an earlier result."
          }
        }
      },
      "CreateScanUrl": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "maxLength": 2048,
            "description": "A public https:// link to the image file. We follow up to 3 redirects and give the download 15 seconds; private and local addresses are refused."
          },
          "model": {
            "type": "string",
            "default": "trueeye-2.3",
            "description": "A model id from `GET /api/v1/models`; `trueeye-2.3` when absent. An unknown id is `422 validation_failed` naming the allowed ids."
          },
          "autoCrop": {
            "type": "boolean",
            "default": true,
            "description": "Borders and app UI around the photo (for example a social post screenshot) are left out, and only the photo is checked; `result.autoCrop.box` says what was kept. `false` checks the whole image as sent. `auto_crop` is accepted too."
          },
          "share": {
            "type": "boolean",
            "default": false,
            "description": "Production mode: `true` also makes the scan’s public result page, also on a `202`; its link comes back as `share.url`. Development mode accepts it and makes no page: its answers repeat an earlier result."
          }
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "const": true,
            "description": "Always `true`."
          }
        }
      }
    },
    "parameters": {
      "Prefer": {
        "name": "Prefer",
        "in": "header",
        "required": false,
        "description": "Production mode: `wait=N` keeps the request open up to N seconds (at most 30) while the scan runs: a scan done in time is answered `200` with `Preference-Applied`. Without it, or when the time runs out, the answer is `202` with `Location` to poll. Development mode ignores it: its answer is always an immediate `200`.",
        "schema": {
          "type": "string",
          "pattern": "^wait=\\d{1,4}$",
          "examples": [
            "wait=30"
          ]
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Makes retries safe: the same key with the same request returns the same scan, without a second charge, for 24 hours. 1 to 255 visible ASCII characters; a UUID is ideal.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255,
          "pattern": "^[\\x21-\\x7E]+$"
        }
      },
      "RequestId": {
        "name": "X-Request-Id",
        "in": "header",
        "required": false,
        "description": "Your own id for this request (8 to 64 letters, digits, dots, colons, dashes or underscores). We use it as the request id; otherwise we make one. Never send a key here: an id that looks like one (`pcx_…`) is replaced, and never stored.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._:-]{8,64}$"
        }
      },
      "ScanId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The scan’s id.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "How many scans per page: 1 to 100, 20 by default.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "The `nextCursor` of the previous page. Omit it for the first page.",
        "schema": {
          "type": "string"
        }
      },
      "StatusFilter": {
        "name": "status",
        "in": "query",
        "required": false,
        "description": "Only scans in this state.",
        "schema": {
          "type": "string",
          "enum": [
            "queued",
            "running",
            "completed",
            "failed"
          ]
        }
      },
      "LabelFilter": {
        "name": "label",
        "in": "query",
        "required": false,
        "description": "Only scans with this verdict.",
        "schema": {
          "type": "string",
          "enum": [
            "ai_generated",
            "inconclusive",
            "not_ai_generated"
          ]
        }
      },
      "SourceFilter": {
        "name": "source",
        "in": "query",
        "required": false,
        "description": "Only scans made on the website (`landing`), in the `app` or with the `api`.",
        "schema": {
          "type": "string",
          "enum": [
            "landing",
            "app",
            "api"
          ]
        }
      },
      "From": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "Only scans created at or after this time (ISO 8601).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "To": {
        "name": "to",
        "in": "query",
        "required": false,
        "description": "Only scans created before this time (ISO 8601).",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "This request’s id. Every answer has one; problems repeat it as `requestId`.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit-Limit": {
        "description": "Requests allowed per window for this key.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the window starts again.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Policy": {
        "description": "The policy, such as `60;w=60`: 60 requests per 60 seconds.",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before polling (202) or retrying (409, 429, 503).",
        "schema": {
          "type": "integer"
        }
      },
      "Location": {
        "description": "The scan to poll: `/api/v1/scans/{id}`.",
        "schema": {
          "type": "string"
        }
      },
      "Preference-Applied": {
        "description": "`wait=N` when your `Prefer: wait` was honoured.",
        "schema": {
          "type": "string"
        }
      },
      "WWW-Authenticate": {
        "description": "With `401`: `Bearer realm=\"perspicax\", error=\"invalid_token\"`.",
        "schema": {
          "type": "string"
        }
      },
      "X-Quota-Limit": {
        "description": "Production answers (errors included) and `GET /api/v1/usage`, always: the scans in the period (Free and Pro: the UTC day’s; Enterprise: the pool).",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Remaining": {
        "description": "The same answers: the scans left in the period.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Quota-Reset": {
        "description": "The same answers: when the period restarts (ISO 8601, UTC): 00:00 UTC, or the end of the Enterprise contract period.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "X-Quota-Overage-Used": {
        "description": "The same answers, when the organisation’s contract has overage on: the overage scans used in the period (a count).",
        "schema": {
          "type": "integer"
        }
      }
    }
  }
}
