{
  "openapi": "3.1.0",
  "info": {
    "title": "Gifi API",
    "version": "1.0.0",
    "summary": "Inspect, clean and rewrite AI provenance marks in text and files.",
    "description": "REST API for inspecting, cleaning and rewriting text and files.\n\n## Authentication\n\nSend an API key as `Authorization: Bearer <key>`. Create one at\nhttps://gifi.ai/api-keys, or obtain a token through OAuth 2.1 with PKCE —\nhttps://gifi.ai/auth.md walks an agent through both.\n\n## Credits\n\nInspecting is free. Cleaning text is free. Cleaning a file costs one credit.\nRewriting costs one credit per candidate. A job that fails is refunded\nautomatically, so a retry after a failure costs nothing beyond the call.\n\n## Retrying safely\n\nSend an `Idempotency-Key` on `POST` operations that spend credits. The key is\nscoped to your account and the endpoint, bound to a hash of the request body,\nand remembered for 24 hours. Retrying with the same key replays the stored\nresponse (marked `Idempotency-Replayed: true`) rather than repeating the work.\nReusing a key with a different body is an error, not a silent replay.\n\n## Sandbox\n\nA key beginning `wmr_test_` runs in sandbox: no subscription required, no\ncredits spent, no job recorded. Authentication, scopes, validation, rate limits\nand the cleaning engine are the real ones — a file really is stripped and the\nactions returned are the actions taken. Rewriting is the exception: it returns\nthis shape with the text unchanged and `rewritten: false`, because calling a\nlanguage model costs real money and a fabricated rewrite would make an\nintegration look correct while the output was a fiction. Sandbox responses\ncarry `sandbox: true` and `creditsCharged: 0`.\n\n## Rate limits\n\nEvery response carries `RateLimit-Limit`, `RateLimit-Remaining` and\n`RateLimit-Reset` (seconds until the window rolls over), so a client can pace\nitself without waiting for a 429. A 429 additionally carries `Retry-After`.\n\n## Errors\n\nFailures are RFC 9457 problem documents served as `application/problem+json`.\nEach names a stable `code`, says whether the request is `retryable`, and\ncarries a `hint` describing what to do next. The legacy `error` field is still\npresent and still holds the human-readable message.\n\n## Versioning and deprecation\n\nThe major version is in the path. Additive changes — a new endpoint, a new\noptional request field, a new response field — ship in place, so clients must\nignore response fields they do not recognise. A breaking change ships under a\nnew path (`/api/v2`) rather than altering `/api/v1` underneath you. When an\noperation is retired it first returns `Deprecation` and `Sunset` response\nheaders (RFC 8594, RFC 9745), with at least 90 days between the announcement\nand the removal.\n\n## What this API will not do\n\nIt will not certify that rewritten text defeats a vendor detector, and it will\nnot claim that stripping a file proves human authorship. Pixel-embedded image\nwatermarks, audio and video are out of scope.",
    "contact": {
      "name": "Gifi support",
      "email": "support@gifi.ai",
      "url": "https://gifi.ai/contact"
    },
    "termsOfService": "https://gifi.ai/terms"
  },
  "servers": [
    {
      "url": "https://gifi.ai",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "API reference",
    "url": "https://gifi.ai/docs"
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Inspect",
      "description": "Report what is present. Never modifies, never charges."
    },
    {
      "name": "Clean",
      "description": "Remove invisible Unicode from text, or metadata from a file."
    },
    {
      "name": "Rewrite",
      "description": "Change the words so a statistical trail is no longer the original sequence."
    },
    {
      "name": "Jobs",
      "description": "Follow billed work after the request that started it."
    },
    {
      "name": "Account",
      "description": "Credit balance, plan limits and recent usage."
    }
  ],
  "paths": {
    "/api/v1/inspect": {
      "post": {
        "operationId": "inspectContent",
        "tags": [
          "Inspect"
        ],
        "summary": "Report AI provenance marks without modifying anything",
        "description": "Send either text or a base64 file. Returns the invisible Unicode, punctuation and confusable findings for text, or the removable metadata for a file. Nothing is changed and no credits are spent, so this is the safe first call before clean or rewrite.",
        "security": [
          {
            "bearerAuth": [
              "inspect"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InspectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was found.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted per minute on the current plan.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window rolls over.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InspectResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/clean": {
      "post": {
        "operationId": "cleanContent",
        "tags": [
          "Clean"
        ],
        "summary": "Strip invisible Unicode from text, or metadata from a file",
        "description": "Text is cleaned for free. A file costs one credit and is returned as base64 with a list of what was removed. Inline files are capped at 3145728 bytes because the platform limits request bodies and base64 inflates a file by a third. Send an Idempotency-Key so a retry after a dropped connection does not clean the file twice.",
        "security": [
          {
            "bearerAuth": [
              "clean"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CleanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cleaned text, or the cleaned file with the actions taken.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted per minute on the current plan.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window rolls over.",
                "schema": {
                  "type": "integer"
                }
              },
              "Location": {
                "description": "The job record for a file clean, for following the work later.",
                "schema": {
                  "type": "string"
                }
              },
              "Idempotency-Replayed": {
                "description": "Present and true when this response was replayed from a previous identical request.",
                "schema": {
                  "type": "boolean"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CleanResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/rewrite": {
      "post": {
        "operationId": "rewriteText",
        "tags": [
          "Rewrite"
        ],
        "summary": "Rewrite text so a statistical trail is no longer the original sequence",
        "description": "Costs one credit per candidate, three by default. Use this when inspect found no removable characters: a SynthID-Text style mark lives in word choice, so there is nothing to delete and the words themselves have to change. Text is capped at 20000 characters. The response never names the host or model that produced it, and it does not certify that any vendor detector will fail.",
        "security": [
          {
            "bearerAuth": [
              "rewrite"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RewriteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The selected rewrite, with the scores behind the choice.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted per minute on the current plan.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window rolls over.",
                "schema": {
                  "type": "integer"
                }
              },
              "Location": {
                "description": "The job record for this rewrite.",
                "schema": {
                  "type": "string"
                }
              },
              "Idempotency-Replayed": {
                "description": "Present and true when this response was replayed from a previous identical request.",
                "schema": {
                  "type": "boolean"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RewriteResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredits"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/jobs": {
      "get": {
        "operationId": "listJobs",
        "tags": [
          "Jobs"
        ],
        "summary": "List this account's jobs, newest first",
        "description": "Cursor-paginated. Read `pagination.nextCursor` and pass it back as `cursor` for the next page; `null` means there are no more. The cursor is anchored to the last row you saw, so jobs created while you page do not shift rows across page boundaries. Free. Default 20 per page, maximum 100.",
        "security": [
          {
            "bearerAuth": [
              "inspect"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobsLimit"
          },
          {
            "$ref": "#/components/parameters/JobsCursor"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of jobs.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted per minute on the current plan.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window rolls over.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/jobs/{id}": {
      "get": {
        "operationId": "getJob",
        "tags": [
          "Jobs"
        ],
        "summary": "Status and result of one job",
        "description": "The poll target for work started by clean or rewrite. A successful billed response carries this URL in its Location header, so a client that lost the connection can follow the job rather than paying to redo it. Free.",
        "security": [
          {
            "bearerAuth": [
              "inspect"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "The job.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted per minute on the current plan.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window rolls over.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/api/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Account"
        ],
        "summary": "Credit balance, plan limits and the last 30 days of usage",
        "description": "Call this before spending to check the balance, and after a run to confirm what was charged. Free, and needs only the inspect scope.",
        "security": [
          {
            "bearerAuth": [
              "inspect"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Balance and recent usage.",
            "headers": {
              "RateLimit-Limit": {
                "description": "Requests permitted per minute on the current plan.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Remaining": {
                "description": "Requests left in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Reset": {
                "description": "Seconds until the current window rolls over.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from https://gifi.ai/api-keys, or an OAuth 2.1 access token. Scopes: clean, inspect, rewrite. A key beginning `wmr_test_` runs in sandbox and spends nothing."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "A unique key for this request, at most 255 characters. Retrying with the same key and the same body replays the original response instead of repeating billed work. Reusing it with a different body returns 422.",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      },
      "JobsLimit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Jobs per page.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "JobsCursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "The `nextCursor` from a previous page. Omit to start from the newest job.",
        "schema": {
          "type": "string"
        }
      },
      "JobId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The job identifier returned by clean or rewrite.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body or a parameter is malformed.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "Missing, invalid, expired or revoked credential.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Authenticated, but the key lacks the scope or the account has no plan.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such record on this account.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "InsufficientCredits": {
        "description": "Not enough credits for this operation.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "IdempotencyInFlight": {
        "description": "An earlier request with this Idempotency-Key is still running.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The inline file exceeds the limit.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "Processing failed, or the Idempotency-Key was reused with a different body. A failed billed job is refunded.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded for the plan.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          },
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Unavailable": {
        "description": "A dependency is unavailable. Retry with backoff.",
        "headers": {
          "RateLimit-Limit": {
            "description": "Requests permitted per minute on the current plan.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Remaining": {
            "description": "Requests left in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the current window rolls over.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "title": "Problem",
        "description": "RFC 9457 problem details. Every failure from this API has this shape.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "hint",
          "retryable",
          "error"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Link to the section of the documentation describing this code."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary of the problem type."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code, repeated in the body."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong with this specific request."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "unauthenticated",
              "insufficient_credits",
              "insufficient_scope",
              "subscription_required",
              "not_found",
              "unsupported_format",
              "payload_too_large",
              "processing_failed",
              "idempotency_in_flight",
              "idempotency_key_reused",
              "rate_limited",
              "internal_error",
              "not_configured",
              "service_unavailable"
            ],
            "description": "Stable machine-readable identifier. Branch on this, not on the message."
          },
          "hint": {
            "type": "string",
            "description": "What the caller should do next."
          },
          "retryable": {
            "type": "boolean",
            "description": "Whether sending the same request again could succeed."
          },
          "refunded": {
            "type": "boolean",
            "description": "Present when a failed billed job had its credits returned."
          },
          "error": {
            "type": "string",
            "description": "The same text as `detail`. Retained for clients written against the original shape."
          }
        }
      },
      "InspectRequest": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "text": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500000
              },
              "aggressive": {
                "type": "boolean"
              }
            },
            "required": [
              "text"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "file": {
                "type": "string",
                "minLength": 1
              },
              "filename": {
                "type": "string",
                "minLength": 1,
                "maxLength": 255
              }
            },
            "required": [
              "file",
              "filename"
            ],
            "additionalProperties": false
          }
        ]
      },
      "CleanRequest": {
        "anyOf": [
          {
            "type": "object",
            "properties": {
              "text": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500000
              },
              "options": {
                "type": "object",
                "properties": {
                  "nfkc": {
                    "type": "boolean"
                  },
                  "punctuation": {
                    "type": "boolean"
                  },
                  "confusables": {
                    "type": "boolean"
                  },
                  "emDash": {
                    "type": "string",
                    "enum": [
                      "keep",
                      "hyphen",
                      "spaced-hyphen",
                      "comma"
                    ]
                  }
                },
                "additionalProperties": false
              }
            },
            "required": [
              "text"
            ],
            "additionalProperties": false
          },
          {
            "type": "object",
            "properties": {
              "file": {
                "type": "string",
                "minLength": 1
              },
              "filename": {
                "type": "string",
                "minLength": 1,
                "maxLength": 255
              }
            },
            "required": [
              "file",
              "filename"
            ],
            "additionalProperties": false
          }
        ]
      },
      "RewriteRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20000
          },
          "strength": {
            "type": "string",
            "enum": [
              "paraphrase",
              "humanize",
              "code",
              "backtranslate",
              "structural"
            ]
          },
          "candidates": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "lang": {
            "type": "string",
            "maxLength": 40
          }
        },
        "required": [
          "text"
        ],
        "additionalProperties": false
      },
      "InspectResponse": {
        "title": "InspectResponse",
        "oneOf": [
          {
            "type": "object",
            "title": "TextInspection",
            "required": [
              "kind",
              "verifiable",
              "bestEffort",
              "creditsCharged"
            ],
            "properties": {
              "kind": {
                "const": "text"
              },
              "length": {
                "type": "integer"
              },
              "verifiable": {
                "type": "object",
                "description": "Countable facts: each suspicious codepoint found, with offsets. Distinct from bestEffort, which is a judgement.",
                "additionalProperties": true
              },
              "bestEffort": {
                "type": "object",
                "description": "The writing-style heuristic. A score, not proof of authorship, and it can flag formulaic human writing.",
                "additionalProperties": true
              },
              "notes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "creditsCharged": {
                "type": "integer",
                "const": 0
              }
            },
            "additionalProperties": true
          },
          {
            "type": "object",
            "title": "FileInspection",
            "required": [
              "kind",
              "format",
              "supported",
              "creditsCharged"
            ],
            "properties": {
              "kind": {
                "const": "file"
              },
              "format": {
                "type": "string"
              },
              "supported": {
                "type": "boolean"
              },
              "handledBy": {
                "type": "string"
              },
              "findings": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "bytesRemoved": {
                "type": "integer"
              },
              "notes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "bytes": {
                "type": "integer"
              },
              "creditsCharged": {
                "type": "integer",
                "const": 0
              },
              "note": {
                "type": "string"
              },
              "sandbox": {
                "type": "boolean",
                "description": "True when a test key served this response."
              }
            },
            "additionalProperties": true
          }
        ]
      },
      "CleanResponse": {
        "title": "CleanResponse",
        "oneOf": [
          {
            "type": "object",
            "title": "TextClean",
            "required": [
              "kind",
              "text",
              "creditsCharged"
            ],
            "properties": {
              "kind": {
                "const": "text"
              },
              "text": {
                "type": "string",
                "description": "The cleaned text."
              },
              "verifiable": {
                "type": "object",
                "description": "Countable facts: each suspicious codepoint found, with offsets. Distinct from bestEffort, which is a judgement.",
                "additionalProperties": true
              },
              "bestEffort": {
                "type": "object",
                "description": "The writing-style heuristic. A score, not proof of authorship, and it can flag formulaic human writing.",
                "additionalProperties": true
              },
              "creditsCharged": {
                "type": "integer",
                "const": 0
              }
            },
            "additionalProperties": true
          },
          {
            "type": "object",
            "title": "FileClean",
            "required": [
              "kind",
              "jobId",
              "file",
              "creditsCharged"
            ],
            "properties": {
              "kind": {
                "const": "file"
              },
              "jobId": {
                "type": "string",
                "format": "uuid"
              },
              "format": {
                "type": "string"
              },
              "handler": {
                "type": "string"
              },
              "actions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "bytesBefore": {
                "type": "integer"
              },
              "bytesAfter": {
                "type": "integer"
              },
              "file": {
                "type": "string",
                "description": "The cleaned file, base64."
              },
              "creditsCharged": {
                "type": "integer",
                "const": 1
              }
            },
            "additionalProperties": true
          }
        ]
      },
      "RewriteResponse": {
        "type": "object",
        "title": "RewriteResponse",
        "required": [
          "kind",
          "jobId",
          "text",
          "creditsCharged"
        ],
        "properties": {
          "kind": {
            "const": "rewrite"
          },
          "sandbox": {
            "type": "boolean",
            "description": "True when a test key served this response."
          },
          "rewritten": {
            "type": "boolean",
            "description": "False in sandbox: the text is returned unchanged and no model was called."
          },
          "jobId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "text": {
            "type": "string",
            "description": "The selected rewrite."
          },
          "strength": {
            "type": "string",
            "enum": [
              "paraphrase",
              "humanize",
              "code",
              "backtranslate",
              "structural"
            ]
          },
          "routing": {
            "type": "object",
            "properties": {
              "nonOrigin": {
                "type": "boolean",
                "description": "Whether the rewrite ran on a provider other than the one that wrote the text."
              }
            }
          },
          "selected": {
            "type": "object",
            "additionalProperties": true
          },
          "candidateCount": {
            "type": "integer"
          },
          "layerA": {
            "type": "object",
            "additionalProperties": true
          },
          "stylometry": {
            "type": "object",
            "properties": {
              "before": {},
              "after": {}
            },
            "description": "Style score before and after. A heuristic, not proof."
          },
          "disclaimers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "detector": {
            "type": "object",
            "description": "Present only when a detector check ran. Never a certificate.",
            "properties": {
              "available": {
                "type": "boolean"
              },
              "score": {
                "type": "number"
              },
              "note": {
                "type": "string"
              }
            }
          },
          "creditsCharged": {
            "type": "integer"
          }
        },
        "additionalProperties": true
      },
      "JobSummary": {
        "type": "object",
        "title": "JobSummary",
        "required": [
          "id",
          "kind",
          "status",
          "source",
          "creditsCost",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "container",
              "rewrite"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "succeeded",
              "failed"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "web",
              "api",
              "mcp"
            ]
          },
          "filename": {
            "type": [
              "string",
              "null"
            ]
          },
          "creditsCost": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "JobDetail": {
        "type": "object",
        "title": "JobDetail",
        "required": [
          "id",
          "kind",
          "status",
          "source",
          "creditsCost",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "container",
              "rewrite"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "succeeded",
              "failed"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "web",
              "api",
              "mcp"
            ]
          },
          "filename": {
            "type": [
              "string",
              "null"
            ]
          },
          "creditsCost": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "byteSize": {
            "type": [
              "integer",
              "null"
            ]
          },
          "report": {
            "type": [
              "object",
              "null"
            ],
            "description": "What the job did. Shape depends on `kind`.",
            "additionalProperties": true
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When any stored output is deleted."
          }
        }
      },
      "JobPage": {
        "type": "object",
        "title": "JobPage",
        "required": [
          "data",
          "pagination"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobSummary"
            }
          },
          "pagination": {
            "type": "object",
            "required": [
              "limit",
              "hasMore",
              "nextCursor"
            ],
            "properties": {
              "limit": {
                "type": "integer"
              },
              "hasMore": {
                "type": "boolean"
              },
              "nextCursor": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Pass as `cursor` for the next page. Null on the last page."
              }
            }
          }
        }
      },
      "UsageResponse": {
        "type": "object",
        "title": "UsageResponse",
        "required": [
          "plan",
          "credits",
          "rateLimit",
          "last30Days"
        ],
        "properties": {
          "plan": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "monthlyCredits": {
                "type": "integer"
              }
            }
          },
          "credits": {
            "type": "object",
            "properties": {
              "granted": {
                "type": "integer",
                "description": "Credits from the current billing period."
              },
              "purchased": {
                "type": "integer",
                "description": "Credits bought as top-ups."
              },
              "total": {
                "type": "integer"
              }
            }
          },
          "rateLimit": {
            "type": "object",
            "properties": {
              "requestsPerMinute": {
                "type": "integer"
              }
            }
          },
          "last30Days": {
            "type": "object",
            "properties": {
              "requests": {
                "type": "integer"
              },
              "creditsUsed": {
                "type": "integer"
              },
              "byEndpoint": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    }
  },
  "x-problem-codes": {
    "invalid_request": {
      "status": 400,
      "title": "Invalid request",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-invalid_request"
    },
    "unauthenticated": {
      "status": 401,
      "title": "Not authenticated",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-unauthenticated"
    },
    "insufficient_credits": {
      "status": 402,
      "title": "Insufficient credits",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-insufficient_credits"
    },
    "insufficient_scope": {
      "status": 403,
      "title": "Missing scope",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-insufficient_scope"
    },
    "subscription_required": {
      "status": 403,
      "title": "No active subscription",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-subscription_required"
    },
    "not_found": {
      "status": 404,
      "title": "Not found",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-not_found"
    },
    "unsupported_format": {
      "status": 415,
      "title": "Unsupported format",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-unsupported_format"
    },
    "payload_too_large": {
      "status": 413,
      "title": "Payload too large",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-payload_too_large"
    },
    "processing_failed": {
      "status": 422,
      "title": "Processing failed",
      "retryable": true,
      "documentation": "https://gifi.ai/docs#error-processing_failed"
    },
    "idempotency_in_flight": {
      "status": 409,
      "title": "Idempotency key already in flight",
      "retryable": true,
      "documentation": "https://gifi.ai/docs#error-idempotency_in_flight"
    },
    "idempotency_key_reused": {
      "status": 422,
      "title": "Idempotency key reused with a different body",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-idempotency_key_reused"
    },
    "rate_limited": {
      "status": 429,
      "title": "Rate limited",
      "retryable": true,
      "documentation": "https://gifi.ai/docs#error-rate_limited"
    },
    "internal_error": {
      "status": 500,
      "title": "Internal error",
      "retryable": true,
      "documentation": "https://gifi.ai/docs#error-internal_error"
    },
    "not_configured": {
      "status": 503,
      "title": "Capability not configured",
      "retryable": false,
      "documentation": "https://gifi.ai/docs#error-not_configured"
    },
    "service_unavailable": {
      "status": 503,
      "title": "Temporarily unavailable",
      "retryable": true,
      "documentation": "https://gifi.ai/docs#error-service_unavailable"
    }
  }
}