{
  "openapi": "3.1.0",
  "info": {
    "title": "Formswrite Public API",
    "version": "1.0.0",
    "summary": "Convert documents into forms and quizzes programmatically.",
    "description": "Public REST API for converting documents (PDF, Word, Google Docs, Google Sheets, images) into Google Forms and 25+ educational platforms. Authenticate with a Formswrite JWT bearer token (see https://formswrite.com/auth.md).\n\n## Errors\nEvery error returns a JSON body matching the `Error` schema (a `message` string plus a boolean `success: false` or `error: true`, and an optional machine-readable `code`). Error pages are never HTML.\n\n## Rate limits\nResponses carry standard `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` headers (RFC 9239 / IETF draft). On `429` a `Retry-After` header tells the agent how many seconds to wait before retrying.\n\n## Idempotency\nSend an `Idempotency-Key` header on `POST /api/v1/convert` to safely retry without creating duplicate jobs. The first response for a key is replayed for 24 hours.\n\n## Pagination\nList endpoints use `limit` (1-100) and `offset` query parameters and return `{ total, limit, offset, items }` so agents can page deterministically.\n\n## Versioning and deprecation\nThe API is versioned in the URL path (`/api/v1`). Every response includes an `X-API-Version` header. Breaking changes ship under a new version prefix; a retiring version is announced at least 90 days ahead with `Deprecation` and `Sunset` response headers (RFC 9745 / RFC 8594) and a notice at https://docs.formswrite.com.",
    "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"
  },
  "servers": [
    {
      "url": "https://api.formswrite.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Conversion",
      "description": "Convert documents into forms and quizzes."
    },
    {
      "name": "Jobs",
      "description": "List and inspect conversion jobs."
    },
    {
      "name": "System",
      "description": "Health, demo, and connection checks."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "oauth2": [
        "forms:read",
        "forms:write",
        "jobs:read"
      ]
    }
  ],
  "paths": {
    "/api/status": {
      "get": {
        "operationId": "healthCheck",
        "summary": "Health check",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up (body \"OK\")."
          }
        }
      }
    },
    "/api/v1/demo/sample-form": {
      "get": {
        "operationId": "getDemoSampleForm",
        "summary": "Sandbox sample conversion (no auth)",
        "description": "Returns a fixed example of a converted form so agents can exercise the response shape end to end without credentials. No documents are processed.",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "A canned sample conversion result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResult"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/convert": {
      "post": {
        "operationId": "convertDocument",
        "summary": "Convert a document to a form",
        "tags": [
          "Conversion"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Document upload (PDF, DOCX, image)."
                  },
                  "fileUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly reachable document URL."
                  },
                  "documentId": {
                    "type": "string",
                    "description": "Google Docs id or URL."
                  },
                  "format": {
                    "type": "string",
                    "description": "Output format, e.g. google_form, formswrite_form, moodle_xml, kahoot."
                  },
                  "documentName": {
                    "type": "string",
                    "description": "Optional name for the created form."
                  },
                  "async": {
                    "type": "boolean",
                    "default": false,
                    "description": "When true, return 202 immediately and poll for status instead of waiting."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion completed (synchronous mode).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobResult"
                }
              }
            }
          },
          "202": {
            "description": "Conversion queued (async mode); poll pollUrl for status.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertAccepted"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "408": {
            "description": "Analysis still in progress; poll pollUrl for status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertAccepted"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/convert/status/{jobId}": {
      "get": {
        "operationId": "getConvertStatus",
        "summary": "Get conversion job status",
        "tags": [
          "Jobs"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "Current job status, plus the result when complete.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/convert/status/{jobId}/stream": {
      "get": {
        "operationId": "streamConvertStatus",
        "summary": "Stream conversion progress (SSE)",
        "description": "Server-Sent Events stream of job progress. Emits `progress` events ({ jobId, state, progress, currentStep }) about once per second and a final `completed` or `failed` event, then closes. Use this for live progress instead of polling.",
        "tags": [
          "Jobs"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/JobId"
          }
        ],
        "responses": {
          "200": {
            "description": "An event stream of progress events.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE frames: `event: progress|completed|failed` with a JSON `data:` line."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/jobs": {
      "get": {
        "operationId": "listJobs",
        "summary": "List your conversion jobs",
        "tags": [
          "Jobs"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "completed",
                "failed"
              ]
            },
            "description": "Filter by job status."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the calling user's conversion jobs.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/v1/zapier/is-valid": {
      "get": {
        "operationId": "validateToken",
        "summary": "Validate a bearer token (connection test)",
        "tags": [
          "System"
        ],
        "responses": {
          "200": {
            "description": "Token is valid; returns the associated user."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "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. The body is signed: verify the `X-Formswrite-Signature` header (`sha256=<hmac>`, HMAC-SHA256 of the raw body using your webhook secret).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookSubmission"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledge receipt with any 2xx."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Formswrite JWT. Generate one at https://formswrite.com/settings/api."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 (authorization code + PKCE). Supports dynamic client registration.",
        "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 and edit forms.",
              "jobs:read": "Read conversion jobs."
            }
          }
        }
      }
    },
    "parameters": {
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Conversion job id."
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Unique key to make POST retries safe. The first response is replayed for 24h."
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        },
        "description": "Page size (1-100)."
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        },
        "description": "Number of items to skip."
      }
    },
    "headers": {
      "RateLimitLimit": {
        "description": "Request quota for the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid bearer token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Plan limit reached; upgrade required.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Present and false on convert/job errors."
          },
          "error": {
            "type": "boolean",
            "description": "Present and true on auth/middleware errors."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code, when applicable (e.g. GOOGLE_AUTH_REQUIRED)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation."
          }
        },
        "examples": [
          {
            "success": false,
            "code": "GOOGLE_AUTH_REQUIRED",
            "message": "Google authentication required for Google Form creation."
          }
        ]
      },
      "ConvertAccepted": {
        "type": "object",
        "required": [
          "success",
          "jobId"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "processing"
            ]
          },
          "jobId": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "pollUrl": {
            "type": "string",
            "description": "Relative URL to poll for status."
          }
        }
      },
      "JobStatus": {
        "type": "object",
        "required": [
          "success",
          "jobId",
          "state"
        ],
        "properties": {
          "success": {
            "type": "boolean"
          },
          "jobId": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "waiting",
              "active",
              "delayed",
              "completed",
              "failed"
            ],
            "description": "Underlying queue state."
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "currentStep": {
            "type": "string"
          },
          "result": {
            "$ref": "#/components/schemas/JobResult"
          },
          "error": {
            "type": "string",
            "description": "Failure reason when state is failed."
          }
        }
      },
      "JobResult": {
        "type": "object",
        "description": "The converted form. Shape depends on the target format.",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "formId": {
            "type": "string"
          },
          "formUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL of the created form."
          },
          "editUrl": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "questionCount": {
            "type": "integer"
          },
          "cached": {
            "type": "boolean",
            "description": "True when replayed from an Idempotency-Key."
          }
        }
      },
      "ConversionJob": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string"
          },
          "documentName": {
            "type": "string"
          },
          "documentType": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "completed",
              "failed"
            ]
          },
          "progress": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "currentStep": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "JobList": {
        "type": "object",
        "required": [
          "total",
          "limit",
          "offset",
          "jobs"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Total matching jobs."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConversionJob"
            }
          }
        }
      },
      "WebhookSubmission": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "form.submission"
            ]
          },
          "formId": {
            "type": "string"
          },
          "submittedAt": {
            "type": "string",
            "format": "date-time"
          },
          "answers": {
            "type": "object",
            "additionalProperties": true
          }
        }
      }
    }
  }
}