{
  "openapi": "3.1.0",
  "info": {
    "title": "Formswrite Public API",
    "version": "1.1.0",
    "summary": "Convert documents into forms and quizzes programmatically.",
    "description": "Public REST API for converting documents (PDF, Word, PowerPoint, Google Docs, images) into Google Forms and 40 other quiz and LMS formats. Authenticate with an OAuth 2.1 access token or a Formswrite API token, sent as a bearer token (see https://formswrite.com/auth.md).\n\n## Errors\n\nEvery error is a JSON object that matches the `Error` schema: a stable machine-readable `code`, a human-readable `message` and a `docs_url`. This holds for unknown routes (404), wrong methods (405, with an `Allow` header), missing or invalid credentials (401, with a `WWW-Authenticate` header naming the protected-resource metadata), missing scope (403), invalid parameters (422, with an `errors` list naming each field), rate limiting (429) and server errors (500). Error pages are never HTML. Older fields (`error`, `success`) are still sent for existing integrations; new clients should branch on `code`.\n\n## Rate limits\n\nLimits are per client IP address: 20 requests per 15 minutes on `POST /api/v1/convert`, and 120 requests per 15 minutes on the index, job, status and sample endpoints. Responses from those endpoints, including 401 and 429, carry `RateLimit-Policy`, `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets). A 429 also carries `Retry-After` in seconds.\n\n## Idempotency\n\nSend an `Idempotency-Key` header on `POST /api/v1/convert` to retry safely without creating duplicate jobs. The first successful response for a key is replayed for 24 hours.\n\n## Pagination\n\n`GET /api/v1/jobs` returns jobs newest first with `has_more` and `next_cursor`. Pass `next_cursor` back as `cursor` to get the next page; a cursor stays valid while jobs are being created. `limit` is 1 to 100 (default 20). `offset` paging and `total` remain available.\n\n## Versioning and deprecation\n\nThe version is in the URL path (`/api/v1`) and in the `X-API-Version` header of every response. Additive changes (new endpoints, new optional parameters, new response fields, new error codes) are made within a version; clients must ignore fields they do not know. Breaking changes ship under a new path prefix. A version that will be retired is announced at least 90 days ahead: every response of that version then carries a `Deprecation` header (RFC 9745) and a `Sunset` header (RFC 8594) with the retirement date, plus `Link` headers with rel=\"deprecation\" and rel=\"sunset\" pointing to https://api.formswrite.com/docs/api#versioning. No version is deprecated today, so those headers are currently absent.",
    "contact": {
      "name": "Formswrite",
      "url": "https://formswrite.com",
      "email": "support@formswrite.com"
    },
    "termsOfService": "https://formswrite.com/terms-of-service",
    "license": {
      "name": "Proprietary",
      "url": "https://formswrite.com/terms-of-service"
    }
  },
  "externalDocs": {
    "description": "Formswrite API documentation",
    "url": "https://docs.formswrite.com/api/overview"
  },
  "servers": [
    {
      "url": "https://api.formswrite.com",
      "description": "Production"
    }
  ],
  "x-deprecation-policy": {
    "notice_days": 90,
    "headers": [
      "Deprecation",
      "Sunset"
    ],
    "url": "https://api.formswrite.com/docs/api#versioning",
    "deprecated_versions": []
  },
  "tags": [
    {
      "name": "Conversion",
      "description": "Convert documents into forms and quizzes."
    },
    {
      "name": "Validation",
      "description": "Check a quiz export file before importing it."
    },
    {
      "name": "Jobs",
      "description": "List and inspect conversion jobs."
    },
    {
      "name": "System",
      "description": "Health, sample data and connection checks."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "oauth2": [
        "forms:read",
        "forms:write",
        "jobs:read"
      ]
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "API index",
        "description": "What this API offers: its version, where it is described and documented, and the list of operations. Needs no credentials.",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The index.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/status": {
      "get": {
        "operationId": "healthCheck",
        "summary": "Health check",
        "description": "Answers the plain text `OK` by default, and a JSON object when the request sends `Accept: application/json`. Needs no credentials and is not rate limited.",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthStatus"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "const": "OK"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/demo/sample-form": {
      "get": {
        "operationId": "getDemoSampleForm",
        "summary": "Sample conversion result (no credentials)",
        "description": "Returns one fixed example of a converted quiz so a client can see the shape of a result, the error format and the rate-limit headers before it has credentials. It is a canned response, not a sandbox: no document is processed and nothing is stored.",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "A fixed sample.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DemoSample"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/validate": {
      "post": {
        "operationId": "validateQuizFile",
        "summary": "Check a quiz export before importing it (no credentials)",
        "description": "Upload a quiz file (Kahoot, Gimkit, Blooket, Wayground or Quizizz spreadsheet, Canvas or other QTI package, Moodle XML, GIFT) and get a report of what the target platform would reject or change. `target` is one of kahoot, gimkit, blooket, quizizz, canvas, moodle, gift, qti, blackboard, brightspace, schoology, and is also read from the query string. Without it the format is detected from the file. QTI is checked structurally (manifest, hrefs inside the zip, one assessmentItem per file, duplicate identifiers) with no schema validation, and reads at most 2 MB of each XML file. The file is read in memory, never stored, and needs no account. Up to 10 MB. A request without a multipart field named `file`, or with more than one file, answers 400 `invalid_request`. `index` in an issue is the zero-based position of the question, or null for the whole file. Rate limited per IP address: 30 requests per 15 minutes.",
        "tags": [
          "Validation"
        ],
        "security": [],
        "parameters": [
          {
            "name": "target",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "kahoot",
                "gimkit",
                "blooket",
                "quizizz",
                "canvas",
                "moodle",
                "gift",
                "qti",
                "blackboard",
                "brightspace",
                "schoology"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ValidateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report. An empty `issues` list means no problems.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateReport"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "415": {
            "$ref": "#/components/responses/UnsupportedMediaType"
          },
          "422": {
            "description": "The file could not be read. The body is a report with one error in `issues` that says why, and also carries `code` and `message`.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateReport"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/validate/report": {
      "post": {
        "operationId": "reportConversionProblem",
        "summary": "Report a quiz file that came out wrong (no credentials)",
        "description": "Tells Formswrite that a checked or converted quiz file came out wrong, so a person can look into it. Send what `POST /api/v1/validate` reported (`target`, `detected`, `summary`, the issue codes) and a `reason`. The file itself is never sent or stored. `reason` is one of missing_questions, wrong_answers, broken_formatting, wont_import, other, else the answer is 400 `invalid_request`. Rate limited per IP address: 10 requests per 15 minutes.",
        "tags": [
          "Validation"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ValidateReportRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report was received.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/convert": {
      "post": {
        "operationId": "convertDocument",
        "summary": "Convert a document to a form",
        "description": "Provide exactly one source (`file`, `fileUrl` or `documentId`) and a target `format`. By default the call waits for the conversion (up to 5 minutes) and returns the result; with `async=true` it returns 202 at once and the job is polled at `pollUrl`. Needs the `forms:write` scope when called with an OAuth token. The account needs a paid plan: on the Free plan this endpoint answers 402 `payment_required`.",
        "tags": [
          "Conversion"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "forms:write"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ConvertRequest"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConvertRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion finished (synchronous mode).",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertResult"
                }
              }
            }
          },
          "202": {
            "description": "Conversion queued (async mode). Poll `pollUrl`.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "408": {
            "description": "Still converting after 5 minutes. The job keeps running: poll `pollUrl`.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertPending"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/convert/status/{jobId}": {
      "get": {
        "operationId": "getConvertStatus",
        "summary": "Get conversion job status",
        "description": "Needs the `jobs:read` scope when called with an OAuth token.",
        "tags": [
          "Jobs"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "jobs:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "The state of the job, with the analysis result once it completed.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/convert/status/{jobId}/stream": {
      "get": {
        "operationId": "streamConvertStatus",
        "summary": "Stream conversion progress (SSE)",
        "description": "Server-Sent Events. Emits a `progress` event about once per second and then one terminal event (`completed`, `failed` or `error`) before closing. Errors that occur before the stream opens (401, 403) are ordinary JSON errors. Not rate limited.",
        "tags": [
          "Jobs"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "jobs:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "An event stream. Each `data:` line is a JSON object.",
            "headers": {
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                },
                "x-event-data": {
                  "$ref": "#/components/schemas/StreamEvent"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/jobs": {
      "get": {
        "operationId": "listJobs",
        "summary": "List your conversion jobs",
        "description": "Newest first. Page with `cursor` (preferred) or `offset`. Needs the `jobs:read` scope when called with an OAuth token.",
        "tags": [
          "Jobs"
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "jobs:read"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "completed",
                "failed"
              ]
            },
            "description": "Only jobs in this status."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the caller's conversion jobs.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    },
    "/api/v1/zapier/is-valid": {
      "get": {
        "operationId": "validateToken",
        "summary": "Check an API token (connection test)",
        "description": "Used by the Zapier and n8n integrations to test a connection. Accepts a Formswrite API token only, not an OAuth access token. Not rate limited.",
        "tags": [
          "System"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The token is valid.",
            "headers": {
              "X-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenCheck"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "default": {
            "$ref": "#/components/responses/UnexpectedError"
          }
        }
      }
    }
  },
  "webhooks": {
    "form.submission": {
      "post": {
        "operationId": "onFormSubmission",
        "summary": "Outbound webhook: a form received a submission",
        "description": "Formswrite POSTs this payload to the webhook URL configured for a form, with the headers `X-Formswrite-Event: form.submission` and `X-Formswrite-Signature: sha256=<hex>`, the HMAC-SHA256 of the raw body under the webhook secret of the form. Verify the signature before trusting the body.",
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookSubmission"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledge with any 2xx within 10 seconds. Anything else is retried, three attempts in all."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Formswrite API token. Create one at https://formswrite.com/dashboard/settings/api."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 authorization code with PKCE (S256) and dynamic client registration. Request the token with resource=https://api.formswrite.com. Walkthrough: https://formswrite.com/auth.md.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://api.formswrite.com/oauth/auth",
            "tokenUrl": "https://api.formswrite.com/oauth/token",
            "refreshUrl": "https://api.formswrite.com/oauth/token",
            "scopes": {
              "forms:read": "Read forms and questions.",
              "forms:write": "Create, edit, export and publish forms.",
              "jobs:read": "Read conversion jobs."
            }
          }
        }
      }
    },
    "parameters": {
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Job id, as returned by POST /api/v1/convert or GET /api/v1/jobs."
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Unique key that makes a retry safe. The first successful response is replayed for 24 hours."
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "description": "Page size."
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 64
        },
        "description": "The `next_cursor` of the previous page. Takes precedence over `offset`."
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "description": "Number of jobs to skip. Prefer `cursor` for walking the whole list."
      }
    },
    "headers": {
      "RateLimitPolicy": {
        "description": "The quota of this endpoint as `<limit>;w=<window seconds>`.",
        "schema": {
          "type": "string",
          "examples": [
            "120;w=900"
          ]
        }
      },
      "RateLimitLimit": {
        "description": "Requests allowed in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the current window ends.",
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before the next request.",
        "schema": {
          "type": "integer"
        }
      },
      "ApiVersion": {
        "description": "The API version that answered.",
        "schema": {
          "type": "string",
          "examples": [
            "1"
          ]
        }
      },
      "Deprecation": {
        "description": "Sent only once this version has been scheduled for retirement (RFC 9745): `@` followed by the Unix time the deprecation took effect.",
        "required": false,
        "schema": {
          "type": "string",
          "examples": [
            "@1798761600"
          ]
        }
      },
      "Sunset": {
        "description": "Sent only once this version has been scheduled for retirement (RFC 8594): the HTTP date after which it stops answering.",
        "required": false,
        "schema": {
          "type": "string",
          "examples": [
            "Thu, 01 Apr 2027 00:00:00 GMT"
          ]
        }
      },
      "Allow": {
        "description": "The methods this route accepts.",
        "schema": {
          "type": "string",
          "examples": [
            "GET, HEAD"
          ]
        }
      },
      "WwwAuthenticate": {
        "description": "RFC 6750 challenge. `resource_metadata` names the RFC 9728 document that says where to get a token; `error` is `invalid_token` or `insufficient_scope` when a token was sent.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is malformed or a required value is missing.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "success": false,
              "code": "invalid_format",
              "message": "Invalid format. Supported formats: google_form, ..."
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The credential is missing, invalid, expired or revoked.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "WWW-Authenticate": {
            "$ref": "#/components/headers/WwwAuthenticate"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "unauthorized",
              "message": "Unauthenticated"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The plan of the account does not include this export.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Payment required",
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "payment_required",
              "needsPayment": true,
              "message": "Upgrade to export to this format."
            }
          }
        }
      },
      "Forbidden": {
        "description": "The credential is valid but lacks the scope or the access this needs.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "WWW-Authenticate": {
            "$ref": "#/components/headers/WwwAuthenticate"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "insufficient_scope",
              "message": "This request needs the jobs:read permission."
            }
          }
        }
      },
      "NotFound": {
        "description": "No such route, or no such job.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "success": false,
              "code": "job_not_found",
              "message": "Job not found"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The route exists but does not accept this method.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Allow": {
            "$ref": "#/components/headers/Allow"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "method_not_allowed",
              "message": "DELETE is not supported here. Allowed: GET, HEAD.",
              "allowed_methods": [
                "GET",
                "HEAD"
              ]
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The uploaded file or the request body is too large.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "file_too_large",
              "message": "The uploaded file is too large."
            }
          }
        }
      },
      "UnsupportedMediaType": {
        "description": "The file is not a quiz format this endpoint knows, or the target is unknown.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "unsupported_media_type",
              "message": "We could not tell what kind of quiz file this is."
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "A parameter has a value that is not acceptable.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "success": false,
              "code": "validation_failed",
              "message": "status must be one of: pending, processing, completed, failed",
              "errors": [
                {
                  "field": "status",
                  "message": "status must be one of: pending, processing, completed, failed"
                }
              ]
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "The rate limit of this endpoint is spent.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "rate_limited",
              "message": "Too many requests. Please slow down and try again shortly."
            }
          }
        }
      },
      "ServerError": {
        "description": "Something failed on the server. Retry with backoff.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "internal_error",
              "message": "Something went wrong on our side. Try again."
            }
          }
        }
      },
      "UnexpectedError": {
        "description": "Any other error. Same body as every error.",
        "headers": {
          "X-API-Version": {
            "$ref": "#/components/headers/ApiVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": true,
              "docs_url": "https://api.formswrite.com/docs/api#errors",
              "code": "internal_error",
              "message": "Something went wrong on our side. Try again."
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The body of every 4xx and 5xx response. Branch on `code`; show `message`.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable identifier. General codes: `invalid_request`, `invalid_json`, `unauthorized`, `payment_required`, `forbidden`, `insufficient_scope`, `not_found`, `method_not_allowed`, `payload_too_large`, `file_too_large`, `validation_failed`, `rate_limited`, `internal_error`. Conversion codes: `missing_source`, `multiple_sources`, `invalid_format`, `unsupported_file_type`, `file_url_rejected`, `invalid_document_id`, `job_not_found`, `still_processing`, `GOOGLE_AUTH_REQUIRED`. New codes may be added; treat an unknown code by its HTTP status."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, safe to show."
          },
          "docs_url": {
            "type": "string",
            "format": "uri",
            "description": "Where the error format and the codes are documented."
          },
          "error": {
            "description": "Legacy marker kept for existing integrations: `true`, or a short label on some older errors. Use `code` instead.",
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string"
              }
            ]
          },
          "success": {
            "type": "boolean",
            "const": false,
            "description": "Legacy marker sent by the conversion endpoints."
          },
          "errors": {
            "type": "array",
            "description": "On `validation_failed`: one entry per rejected field.",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          },
          "allowed_methods": {
            "type": "array",
            "description": "On `method_not_allowed`.",
            "items": {
              "type": "string"
            }
          },
          "supported_formats": {
            "type": "array",
            "description": "On `invalid_format`.",
            "items": {
              "type": "string"
            }
          },
          "needsPayment": {
            "type": "boolean",
            "description": "On `payment_required`."
          }
        },
        "additionalProperties": true
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "api_version",
          "openapi",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "api_version": {
            "type": "string",
            "examples": [
              "1"
            ]
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "authentication": {
            "type": "string",
            "format": "uri"
          },
          "status": {
            "type": "string",
            "format": "uri"
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "method",
                "path",
                "operation_id"
              ],
              "properties": {
                "method": {
                  "type": "string"
                },
                "path": {
                  "type": "string"
                },
                "operation_id": {
                  "type": "string"
                },
                "summary": {
                  "type": "string"
                },
                "auth": {
                  "type": "boolean",
                  "description": "Whether the operation needs a credential."
                }
              }
            }
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "required": [
          "status",
          "api_version"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "api_version": {
            "type": "string",
            "examples": [
              "1"
            ]
          }
        }
      },
      "ValidateRequest": {
        "type": "object",
        "required": [
          "file"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "The quiz file, up to 10 MB."
          },
          "target": {
            "type": "string",
            "enum": [
              "kahoot",
              "gimkit",
              "blooket",
              "quizizz",
              "canvas",
              "moodle",
              "gift",
              "qti",
              "blackboard",
              "brightspace",
              "schoology"
            ]
          }
        }
      },
      "ValidateReport": {
        "type": "object",
        "required": [
          "detected",
          "target",
          "summary",
          "questionTypes",
          "issues"
        ],
        "properties": {
          "detected": {
            "type": [
              "string",
              "null"
            ],
            "description": "The format the parser read, for example moodle_xml, qti_2 or gift. Null when the file could not be read."
          },
          "target": {
            "type": "string",
            "enum": [
              "kahoot",
              "gimkit",
              "blooket",
              "quizizz",
              "canvas",
              "moodle",
              "gift",
              "qti",
              "blackboard",
              "brightspace",
              "schoology"
            ]
          },
          "summary": {
            "type": "object",
            "required": [
              "questions",
              "errors",
              "warnings",
              "info"
            ],
            "properties": {
              "questions": {
                "type": "integer"
              },
              "errors": {
                "type": "integer"
              },
              "warnings": {
                "type": "integer"
              },
              "info": {
                "type": "integer"
              }
            }
          },
          "questionTypes": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Question count by type, for example MULTIPLE_CHOICE."
          },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "code",
                "severity",
                "index",
                "title",
                "message",
                "fix"
              ],
              "properties": {
                "code": {
                  "type": "string",
                  "example": "kahoot_question_too_long"
                },
                "severity": {
                  "type": "string",
                  "enum": [
                    "error",
                    "warning",
                    "info"
                  ]
                },
                "index": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "Zero-based question position, null for the whole file."
                },
                "title": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "fix": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ValidateReportRequest": {
        "type": "object",
        "required": [
          "reason"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "missing_questions",
              "wrong_answers",
              "broken_formatting",
              "wont_import",
              "other"
            ]
          },
          "comment": {
            "type": "string",
            "maxLength": 4000,
            "description": "What went wrong, in your words."
          },
          "target": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "kahoot",
              "gimkit",
              "blooket",
              "quizizz",
              "canvas",
              "moodle",
              "gift",
              "qti",
              "blackboard",
              "brightspace",
              "schoology",
              null
            ]
          },
          "detected": {
            "type": [
              "string",
              "null"
            ]
          },
          "fileName": {
            "type": "string",
            "maxLength": 120
          },
          "summary": {
            "type": "object",
            "properties": {
              "questions": {
                "type": "integer"
              },
              "errors": {
                "type": "integer"
              },
              "warnings": {
                "type": "integer"
              },
              "info": {
                "type": "integer"
              }
            }
          },
          "issueCodes": {
            "type": "string",
            "maxLength": 200,
            "description": "Comma-separated issue codes from the report."
          },
          "page": {
            "type": "string",
            "maxLength": 200
          }
        }
      },
      "ReportAccepted": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "ConvertRequest": {
        "type": "object",
        "required": [
          "format"
        ],
        "description": "Exactly one of `file`, `fileUrl` and `documentId`. `file` needs multipart/form-data.",
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "The document to convert, up to 10 MB: PDF, Word, PowerPoint, Excel, CSV, text or an image."
          },
          "fileUrl": {
            "type": "string",
            "format": "uri",
            "description": "Publicly reachable URL of the document."
          },
          "documentId": {
            "type": "string",
            "description": "Google Docs id or URL. The document must be readable by Formswrite."
          },
          "format": {
            "type": "string",
            "enum": [
              "google_form",
              "formswrite_form",
              "canvas",
              "moodle",
              "blackboard_pool",
              "blackboard_test",
              "blackboard_tsv",
              "brightspace",
              "schoology_assessment",
              "schoology_new_assessment",
              "itslearning",
              "sls",
              "tao_testing",
              "coursera",
              "qti_12",
              "adobe_captivate",
              "studymate",
              "zoom",
              "cypher_lms",
              "talent_lms",
              "learn365",
              "coursearc",
              "schoology_item_bank",
              "sakai",
              "kahoot",
              "quizizz",
              "blooket",
              "gimkit",
              "socrative",
              "wooclap",
              "quizalize",
              "classmarker",
              "pear_assessment",
              "qti_21",
              "qti_22",
              "gift",
              "aiken",
              "cloze",
              "word",
              "learndash",
              "h5p"
            ],
            "description": "Target format."
          },
          "documentName": {
            "type": "string",
            "description": "Name for the created form."
          },
          "async": {
            "type": "boolean",
            "default": false,
            "description": "Return 202 at once instead of waiting for the result."
          }
        }
      },
      "ConvertAccepted": {
        "type": "object",
        "required": [
          "success",
          "jobId",
          "pollUrl"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "status": {
            "type": "string",
            "const": "processing"
          },
          "jobId": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "pollUrl": {
            "type": "string",
            "description": "Path to poll, relative to the API origin."
          }
        }
      },
      "ConvertPending": {
        "type": "object",
        "required": [
          "code",
          "message",
          "jobId",
          "pollUrl"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "boolean"
          },
          "code": {
            "type": "string",
            "const": "still_processing"
          },
          "status": {
            "type": "string",
            "const": "processing"
          },
          "jobId": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "pollUrl": {
            "type": "string"
          },
          "docs_url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "ConvertResult": {
        "type": "object",
        "description": "What was created. File formats return a download link; `google_form` and `formswrite_form` return the form. Fields that do not apply are absent.",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "format": {
            "type": "string"
          },
          "exportId": {
            "type": [
              "integer",
              "string"
            ],
            "description": "File formats: id of the export."
          },
          "downloadUrl": {
            "type": "string",
            "format": "uri",
            "description": "File formats: where to download the file."
          },
          "fileName": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "File formats: when the download link expires."
          },
          "formId": {
            "type": "string",
            "description": "Google Forms: the form id."
          },
          "formUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The URL of the created form."
          },
          "questionCount": {
            "type": "integer"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "idempotentReplay": {
            "type": "boolean",
            "description": "True when replayed for an Idempotency-Key."
          }
        },
        "additionalProperties": true
      },
      "JobStatus": {
        "type": "object",
        "required": [
          "success",
          "jobId",
          "state"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "jobId": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "description": "Queue state of the job.",
            "examples": [
              "waiting",
              "active",
              "delayed",
              "completed",
              "failed"
            ]
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "currentStep": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "description": "Present when `state` is `completed`: the analysed document (title, questions).",
            "additionalProperties": true
          },
          "error": {
            "type": "string",
            "description": "Present when `state` is `failed`: the reason."
          }
        }
      },
      "StreamEvent": {
        "type": "object",
        "description": "The JSON in the `data:` line of each event.",
        "properties": {
          "jobId": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "currentStep": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": true
          },
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ConversionJob": {
        "type": "object",
        "required": [
          "jobId",
          "status",
          "createdAt"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "documentName": {
            "type": [
              "string",
              "null"
            ]
          },
          "documentType": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "currentStep": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "JobList": {
        "type": "object",
        "required": [
          "total",
          "limit",
          "offset",
          "has_more",
          "next_cursor",
          "jobs"
        ],
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConversionJob"
            }
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether another page follows."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as `cursor` to get the next page; null on the last page."
          },
          "total": {
            "type": "integer",
            "description": "Jobs matching the filter, across all pages."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied (0 when paging by cursor)."
          }
        }
      },
      "DemoSample": {
        "type": "object",
        "required": [
          "success",
          "demo",
          "format",
          "result"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "demo": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string"
          },
          "format": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string"
              },
              "formUrl": {
                "type": "string",
                "format": "uri"
              },
              "questionCount": {
                "type": "integer"
              },
              "questions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "answer": {
                      "type": "string"
                    },
                    "answers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "TokenCheck": {
        "type": "object",
        "required": [
          "success"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "jwt": {
            "type": "object",
            "description": "The claims of the token.",
            "additionalProperties": true
          },
          "user": {
            "type": "object",
            "description": "The account the token acts as.",
            "additionalProperties": true
          }
        }
      },
      "WebhookSubmission": {
        "type": "object",
        "required": [
          "event",
          "formId",
          "responseId",
          "submittedAt"
        ],
        "properties": {
          "event": {
            "type": "string",
            "const": "form.submission"
          },
          "formId": {
            "type": [
              "integer",
              "string"
            ]
          },
          "publicId": {
            "type": "string"
          },
          "responseId": {
            "type": [
              "integer",
              "string"
            ]
          },
          "submittedAt": {
            "type": "string",
            "format": "date-time"
          },
          "answers": {
            "description": "The submitted answers. Absent for end-to-end encrypted forms."
          },
          "respondentEmail": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    }
  }
}