{
  "openapi": "3.1.0",
  "info": {
    "title": "myocr.app API",
    "version": "1.0.0",
    "description": "Convert PDF and image documents to structured Excel/text. Tables and full-text run on our OCR engine; specialized models return domain-specific fields.\n\n**Base URL:** `https://api.myocr.app`\n\n**Authentication:** `X-API-Key` header. Get a key from your [dashboard](/account/api).\nTest keys (`sk_test_...`) and live keys (`sk_live_...`) are supported.\n\n**Rate limits** (per API key \u2014 clients behind shared NAT/proxy do NOT share the limit):\n- `/v1/convert` (sync): 60 requests/min\n- `/v1/jobs` (async): 120 requests/min\n- `/v1/batch` (multi-file): 30 requests/min\n\nRequests without an API key (`/v1/status`) are rate-limited per IP.\n\n**Response envelope:**\n```json\n{\"success\": true, \"data\": {...}, \"request_id\": \"abcd1234\"}\n```\nOr on error:\n```json\n{\"success\": false, \"error\": {\"code\": \"INVALID_API_KEY\", \"message\": \"...\"}, \"request_id\": \"...\"}\n```\n\nAll responses include `X-MyOCR-Request-Id` header. File responses also include `X-MyOCR-Pages-Used` and `X-MyOCR-Model`.\n",
    "contact": {
      "name": "myocr.app support",
      "email": "info@myocr.app",
      "url": "https://www.myocr.app"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.myocr.app",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Status",
      "description": "Public health check"
    },
    {
      "name": "Convert",
      "description": "Synchronous conversion (\u22645MB, \u226410 pages)"
    },
    {
      "name": "Jobs",
      "description": "Asynchronous conversion (up to 50MB, up to 500 pages per PDF, with webhook)"
    },
    {
      "name": "Keys",
      "description": "API key management (requires session login, not API key)"
    }
  ],
  "paths": {
    "/v1/status": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Public health check",
        "description": "No authentication required. Returns service flags and limits.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/convert": {
      "post": {
        "tags": [
          "Convert"
        ],
        "summary": "Sync document conversion",
        "description": "Multipart upload. Max 5MB, 10 pages. Synchronous \u2014 response contains the result file (or JSON if `output=json`).\n\nFor larger or batched jobs use `/v1/jobs`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "model"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "PDF or image (pdf/png/jpg/jpeg/tiff/tif/bmp/heif)"
                  },
                  "model": {
                    "type": "string",
                    "enum": [
                      "tables",
                      "text",
                      "invoice",
                      "receipt",
                      "bank_statement",
                      "business_card"
                    ],
                    "default": "tables"
                  },
                  "output": {
                    "type": "string",
                    "enum": [
                      "xlsx",
                      "txt",
                      "json",
                      "csv"
                    ],
                    "default": "xlsx",
                    "description": "Override default output format. `text` model always returns txt.\n`csv` exports only the first sheet of the structured workbook (use xlsx for multi-sheet).\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion successful",
            "headers": {
              "X-MyOCR-Request-Id": {
                "schema": {
                  "type": "string"
                }
              },
              "X-MyOCR-Pages-Used": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-MyOCR-Model": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "XLSX file with structured fields based on model"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "Plain text (model=text or output=txt)"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          },
          "502": {
            "$ref": "#/components/responses/OcrError"
          }
        }
      }
    },
    "/v1/jobs": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "List async jobs for the calling API key (paginated)",
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "done",
                "failed"
              ]
            }
          },
          {
            "in": "query",
            "name": "model",
            "schema": {
              "type": "string",
              "enum": [
                "tables",
                "text",
                "invoice",
                "receipt",
                "bank_statement",
                "business_card"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated job list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "total": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "jobs": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/JobStateResponse/properties/data"
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/batch": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Create async batch (multiple files)",
        "description": "Submit up to 20 files (\u226450MB each, \u2264500 pages per PDF) in a single multipart request.\nEach file becomes a separate ApiJob and consumes one quota call. Returns array of\n`request_id` (one per file) plus an `errors` array for files that failed validation\n(e.g. `FILE_TOO_LARGE`, `TOO_MANY_PAGES`, `UNSUPPORTED_FILE_TYPE`).\n",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "files",
                  "model"
                ],
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "1\u201320 files (pdf/png/jpg/...)"
                  },
                  "model": {
                    "type": "string",
                    "enum": [
                      "tables",
                      "text",
                      "invoice",
                      "receipt",
                      "bank_statement",
                      "business_card"
                    ],
                    "description": "Single model applied to all files in batch"
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Notified once per job (job.completed/failed for each file)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch accepted (some jobs may have errors \u2014 check `errors` array)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "batch_id": {
                          "type": "string"
                        },
                        "model": {
                          "type": "string"
                        },
                        "jobs_created": {
                          "type": "integer"
                        },
                        "jobs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "filename": {
                                "type": "string"
                              },
                              "request_id": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "errors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "filename": {
                                "type": "string"
                              },
                              "code": {
                                "type": "string"
                              },
                              "message": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    "request_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/QuotaExceeded"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimit"
          }
        }
      }
    },
    "/v1/jobs/{request_id}": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Poll job status",
        "parameters": [
          {
            "in": "path",
            "name": "request_id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStateResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Delete job + cleanup files",
        "parameters": [
          {
            "in": "path",
            "name": "request_id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/jobs/{request_id}/result": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Download job result",
        "description": "Returns a signed URL (24h, R2-backed) or streams the result file directly.\nOnly available when `status == \"done\"`.\n",
        "parameters": [
          {
            "in": "path",
            "name": "request_id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signed URL or binary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultUrlResponse"
                }
              },
              "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Job not done yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Status"
        ],
        "summary": "Current month quota usage for the calling API key",
        "description": "Returns pages used vs limit for the current period for the API key passed in X-API-Key.\nBilling is per page: `calls_used`/`calls_limit` are counted in PAGES (1 unit = 1 page).\nThe Free plan includes 10 pages one-time (not per month); paid plans reset at renewal.\nUseful for scripts that monitor quota programmatically (e.g. to upgrade before exhaustion).\n",
        "responses": {
          "200": {
            "description": "Usage data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "plan": {
                          "type": "string",
                          "nullable": true,
                          "example": "free"
                        },
                        "calls_used": {
                          "type": "integer",
                          "description": "Pages used this period (1 unit = 1 page).",
                          "example": 4
                        },
                        "calls_limit": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Included pages (null = unlimited/pay-as-you-go). Free = 10 (one-time).",
                          "example": 10
                        },
                        "percentage": {
                          "type": "number",
                          "nullable": true,
                          "example": 40.0
                        },
                        "reset_date": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "year_month": {
                          "type": "string",
                          "example": "2026-05"
                        },
                        "is_test_key": {
                          "type": "boolean"
                        }
                      }
                    },
                    "request_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "description": "Billing module not registered (deployment misconfigured)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys": {
      "post": {
        "tags": [
          "Keys"
        ],
        "summary": "Create API key",
        "description": "Requires logged-in session (cookie auth, not X-API-Key).\nThe raw key is returned **once** \u2014 store it now.\n",
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "is_test": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeyCreatedResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Keys"
        ],
        "summary": "List own API keys",
        "security": [],
        "responses": {
          "200": {
            "description": "List",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKeyInfo"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/{id}/rotate": {
      "post": {
        "tags": [
          "Keys"
        ],
        "summary": "Rotate an API key (atomic revoke + create)",
        "description": "Revokes the existing key and creates a new one with the same `label`, `is_test` and\n`monthly_limit`. The new raw key is returned **once** \u2014 store it now.\nUse this when you suspect a key was leaked or as part of periodic rotation policy.\n",
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rotated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "old_id": {
                          "type": "integer"
                        },
                        "old_revoked": {
                          "type": "boolean"
                        },
                        "id": {
                          "type": "integer"
                        },
                        "key": {
                          "type": "string",
                          "description": "New raw key \u2014 shown once"
                        },
                        "prefix": {
                          "type": "string"
                        },
                        "label": {
                          "type": "string"
                        },
                        "is_test": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/{id}": {
      "delete": {
        "tags": [
          "Keys"
        ],
        "summary": "Revoke an API key",
        "security": [],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SuccessResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Send your API key in the `X-API-Key` header. Get one at /account/api."
      }
    },
    "schemas": {
      "StatusResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "service": {
                "type": "string"
              },
              "version": {
                "type": "string"
              },
              "environment": {
                "type": "string"
              },
              "models_supported": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "features": {
                "type": "object",
                "properties": {
                  "auth": {
                    "type": "boolean"
                  },
                  "sync_convert": {
                    "type": "boolean"
                  },
                  "async_jobs": {
                    "type": "boolean"
                  },
                  "webhook": {
                    "type": "boolean"
                  },
                  "prebuilt_models_enabled": {
                    "type": "boolean"
                  },
                  "redis_configured": {
                    "type": "boolean"
                  },
                  "r2_configured": {
                    "type": "boolean"
                  }
                }
              },
              "limits": {
                "type": "object",
                "properties": {
                  "sync_max_bytes": {
                    "type": "integer",
                    "example": 5242880
                  },
                  "sync_max_pages": {
                    "type": "integer",
                    "example": 10
                  },
                  "jobs_max_bytes": {
                    "type": "integer",
                    "example": 52428800
                  },
                  "sync_rate_per_minute": {
                    "type": "integer",
                    "example": 60
                  },
                  "jobs_rate_per_minute": {
                    "type": "integer",
                    "example": 120
                  }
                }
              }
            }
          }
        }
      },
      "SuccessResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": true
          },
          "data": {
            "type": "object"
          },
          "request_id": {
            "type": "string",
            "example": "abcd1234efgh5678"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "MISSING_API_KEY",
                  "INVALID_API_KEY",
                  "UNSUPPORTED_MODEL",
                  "UNSUPPORTED_OUTPUT",
                  "UNSUPPORTED_FILE_TYPE",
                  "MISSING_FILE",
                  "INVALID_FILE",
                  "INVALID_PARAMETER",
                  "FILE_TOO_LARGE",
                  "TOO_MANY_PAGES",
                  "INVALID_WEBHOOK_URL",
                  "CARD_REQUIRED",
                  "NO_ACTIVE_PLAN",
                  "INSUFFICIENT_PAGES",
                  "QUOTA_EXCEEDED",
                  "SPEND_CAP_REACHED",
                  "SMART_EXTRACT_LIMIT",
                  "NOT_READY",
                  "NOT_FOUND",
                  "SERVICE_BUSY",
                  "OCR_ERROR",
                  "STORAGE_ERROR",
                  "SERVICE_NOT_READY",
                  "INTERNAL_ERROR"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "JobCreatedResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "request_id": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending"
                ]
              },
              "model": {
                "type": "string"
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "JobStateResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "request_id": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "processing",
                  "done",
                  "failed"
                ]
              },
              "model": {
                "type": "string"
              },
              "pages_used": {
                "type": "integer",
                "nullable": true
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "completed_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "error_detail": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "ResultUrlResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "result_url": {
                "type": "string",
                "format": "uri",
                "description": "Signed URL, 24h expiry"
              },
              "expires_in": {
                "type": "integer",
                "example": 86400
              }
            }
          }
        }
      },
      "KeyCreatedResponse": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "key": {
                "type": "string",
                "description": "Raw key \u2014 shown once, store securely",
                "example": "sk_live_..."
              },
              "prefix": {
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "is_test": {
                "type": "boolean"
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              },
              "note": {
                "type": "string"
              }
            }
          }
        }
      },
      "ApiKeyInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "prefix": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "is_active": {
            "type": "boolean"
          },
          "is_test": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation error (missing/invalid field)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "QuotaExceeded": {
        "description": "Monthly quota exhausted",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "File too large or too many pages",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimit": {
        "description": "Rate limit exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "OcrError": {
        "description": "Upstream OCR engine error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "StorageError": {
        "description": "Object storage (R2) error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    }
  }
}