{
  "openapi": "3.0.3",
  "info": {
    "title": "TableForge API",
    "description": "Extract tables from PDF documents, Word files, and images to Excel, CSV, Markdown, or structured JSON using AI. The API is async: submit a job, poll for completion, retrieve results.\n\n**Authentication**: All endpoints (except `POST /api/v1/auth/register`) require a Bearer API key in the `Authorization` header. Keys have the format `tf_live_...` and are issued on account registration or via `POST /api/v1/auth/api-keys`.\n\n**Extraction modes and credit costs**:\n- `table` (1× credit/page) — fast extraction, preserves basic structure\n- `data_clean` (2× credits/page) — normalized output ideal for database import\n- `markdown` (2× credits/page) — LLM/RAG-friendly Markdown tables\n- `layout_match` (4× credits/page) — pixel-perfect fidelity for legal/compliance\n\n**Job lifecycle**: `queued` → `processing` → `complete` | `failed` | `cancelled`",
    "version": "1.0.0",
    "contact": {
      "email": "support@tableforge.ai",
      "url": "https://tableforge.ai/support"
    },
    "termsOfService": "https://tableforge.ai/terms"
  },
  "servers": [
    {
      "url": "https://tableforge.ai",
      "description": "Production"
    }
  ],
  "security": [
    { "bearerAuth": [] }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key with format `tf_live_...`. Obtain via `POST /api/v1/auth/register` or `POST /api/v1/auth/api-keys`."
      }
    },
    "schemas": {
      "ApiKey": {
        "type": "object",
        "properties": {
          "keyId": { "type": "string", "example": "tfk_abc123" },
          "name": { "type": "string", "example": "Production Bot" },
          "scopes": { "type": "array", "items": { "type": "string" }, "example": ["extract"] },
          "createdAt": { "type": "string", "format": "date-time" },
          "lastUsedAt": { "type": "string", "format": "date-time" },
          "isActive": { "type": "boolean" }
        }
      },
      "JobStatus": {
        "type": "string",
        "enum": ["queued", "processing", "complete", "failed", "cancelled"]
      },
      "ExtractionMode": {
        "type": "string",
        "enum": ["table", "data_clean", "markdown", "layout_match"],
        "description": "table=1× credit/page, data_clean=2×, markdown=2×, layout_match=4×"
      },
      "Job": {
        "type": "object",
        "properties": {
          "jobId": { "type": "string", "example": "job_abc123" },
          "status": { "$ref": "#/components/schemas/JobStatus" },
          "mode": { "$ref": "#/components/schemas/ExtractionMode" },
          "fileName": { "type": "string", "example": "report.pdf" },
          "fileSize": { "type": "integer", "description": "Bytes" },
          "pageCount": { "type": "integer" },
          "creditsConsumed": { "type": "integer" },
          "createdAt": { "type": "string", "format": "date-time" },
          "startedAt": { "type": "string", "format": "date-time" },
          "completedAt": { "type": "string", "format": "date-time" },
          "failedAt": { "type": "string", "format": "date-time" },
          "errorCode": { "type": "string" },
          "errorMessage": { "type": "string" },
          "links": {
            "type": "object",
            "properties": {
              "result": { "type": "string", "example": "/api/v1/jobs/job_abc123/result" }
            }
          }
        }
      },
      "TableCell": {
        "type": "object",
        "properties": {
          "content": { "type": "string" },
          "rowIndex": { "type": "integer" },
          "columnIndex": { "type": "integer" },
          "rowSpan": { "type": "integer" },
          "columnSpan": { "type": "integer" }
        }
      },
      "ExtractedTable": {
        "type": "object",
        "properties": {
          "pageNumber": { "type": "integer" },
          "rowCount": { "type": "integer" },
          "columnCount": { "type": "integer" },
          "cells": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/TableCell" }
          }
        }
      },
      "QuotaMeter": {
        "type": "object",
        "properties": {
          "used": { "type": "integer" },
          "limit": { "type": "integer" },
          "remaining": { "type": "integer" },
          "percentUsed": { "type": "number" },
          "resetAt": { "type": "string", "format": "date-time" }
        }
      },
      "AccountStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": ["active", "approaching_limit", "quota_exceeded", "overage_active", "cap_reached", "suspended", "payment_required"],
            "description": "Machine-readable status code. `approaching_limit` = ≥80% quota used. `overage_active` = over quota but overage billing enabled. `cap_reached` = spending cap hit."
          },
          "plan": { "type": "string", "example": "pro" },
          "quota": {
            "type": "object",
            "properties": {
              "scanPages": { "$ref": "#/components/schemas/QuotaMeter" },
              "extractionCredits": { "$ref": "#/components/schemas/QuotaMeter" }
            }
          },
          "overage": {
            "type": "object",
            "properties": {
              "enabled": { "type": "boolean" },
              "spendingCapCents": { "type": "integer", "nullable": true, "description": "null = unlimited" },
              "currentOverageSpendCents": { "type": "integer" },
              "rates": {
                "type": "object",
                "properties": {
                  "table": { "type": "integer", "description": "Cents per page" },
                  "data_clean": { "type": "integer" },
                  "markdown": { "type": "integer" },
                  "layout_match": { "type": "integer" }
                }
              }
            }
          },
          "actions": {
            "type": "object",
            "properties": {
              "canExtract": { "type": "boolean" },
              "canUpgrade": { "type": "boolean" },
              "upgradeUrl": { "type": "string" },
              "addOverageUrl": { "type": "string" }
            }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "webhookId": { "type": "string", "example": "wh_abc123" },
          "url": { "type": "string", "format": "uri" },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": ["job.completed", "job.failed", "quota.approaching", "quota.exceeded", "overage.cap_reached"]
            }
          },
          "isActive": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" },
          "lastDeliveryAt": { "type": "string", "format": "date-time" },
          "lastStatus": { "type": "integer" },
          "failureCount": { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "code": { "type": "string" },
          "message": { "type": "string" }
        }
      }
    }
  },
  "paths": {
    "/api/v1/auth/register": {
      "post": {
        "summary": "Create an agent account",
        "description": "Programmatically create a new account. Returns the first API key immediately — no human verification step required. The key is shown only once; store it securely.",
        "operationId": "registerAgent",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "minLength": 8 },
                  "name": { "type": "string", "description": "Display name (optional)" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "userId": { "type": "string" },
                    "email": { "type": "string" },
                    "createdAt": { "type": "string", "format": "date-time" },
                    "apiKey": {
                      "type": "object",
                      "properties": {
                        "keyId": { "type": "string" },
                        "key": { "type": "string", "description": "Raw key — shown once only" },
                        "scopes": { "type": "array", "items": { "type": "string" } }
                      }
                    },
                    "note": { "type": "string" }
                  }
                }
              }
            }
          },
          "409": { "description": "Email already registered", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/auth/api-keys": {
      "get": {
        "summary": "List API keys",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": { "type": "array", "items": { "$ref": "#/components/schemas/ApiKey" } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an API key",
        "description": "The raw key is returned once on creation and is not retrievable afterwards.",
        "operationId": "createApiKey",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string", "example": "CI Bot" },
                  "scopes": { "type": "array", "items": { "type": "string" }, "example": ["extract"] }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/ApiKey" },
                    { "type": "object", "properties": { "key": { "type": "string" }, "note": { "type": "string" } } }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/api-keys/{keyId}": {
      "delete": {
        "summary": "Revoke an API key",
        "operationId": "revokeApiKey",
        "parameters": [{ "name": "keyId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "description": "Revoked", "content": { "application/json": { "schema": { "type": "object", "properties": { "revoked": { "type": "boolean" }, "keyId": { "type": "string" } } } } } },
          "404": { "description": "Key not found" }
        }
      }
    },
    "/api/v1/jobs/upload-url": {
      "post": {
        "summary": "Get a pre-signed upload URL",
        "description": "Use this for files larger than a few MB. Upload directly to Azure Blob Storage with the returned URL, then reference the blob in `POST /api/v1/jobs`.",
        "operationId": "getUploadUrl",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["fileName"],
                "properties": {
                  "fileName": { "type": "string" },
                  "fileSize": { "type": "integer" },
                  "contentType": { "type": "string", "default": "application/pdf" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "uploadUrl": { "type": "string", "description": "PUT this URL with file bytes" },
                    "containerName": { "type": "string" },
                    "blobName": { "type": "string" },
                    "expiresAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs": {
      "post": {
        "summary": "Submit an extraction job",
        "description": "Two upload modes:\n- **Multipart** (`multipart/form-data`): include `file` field with binary and optional `mode` field.\n- **Blob reference** (`application/json`): after uploading via `POST /api/v1/jobs/upload-url`, pass `{ blobReference: { containerName, blobName }, mode }`.\n\nReturns `202 Accepted` with a `jobId`. Poll `GET /api/v1/jobs/{jobId}` for status.",
        "operationId": "submitJob",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": { "type": "string", "format": "binary" },
                  "mode": { "$ref": "#/components/schemas/ExtractionMode" }
                }
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["blobReference"],
                "properties": {
                  "blobReference": {
                    "type": "object",
                    "properties": {
                      "containerName": { "type": "string" },
                      "blobName": { "type": "string" },
                      "fileName": { "type": "string" }
                    }
                  },
                  "mode": { "$ref": "#/components/schemas/ExtractionMode" }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Job queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string" },
                    "status": { "type": "string", "example": "queued" },
                    "mode": { "$ref": "#/components/schemas/ExtractionMode" },
                    "fileName": { "type": "string" },
                    "estimatedWaitSeconds": { "type": "integer" },
                    "links": {
                      "type": "object",
                      "properties": {
                        "status": { "type": "string" },
                        "result": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{jobId}": {
      "get": {
        "summary": "Poll job status",
        "operationId": "getJobStatus",
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
          "404": { "description": "Job not found" }
        }
      },
      "delete": {
        "summary": "Cancel a running job",
        "operationId": "cancelJob",
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "cancelled": { "type": "boolean" }, "jobId": { "type": "string" } } } } } },
          "409": { "description": "Job already in terminal state" }
        }
      }
    },
    "/api/v1/jobs/{jobId}/result": {
      "get": {
        "summary": "Retrieve extraction result",
        "description": "Returns `202` if the job is not yet complete. For `table`, `data_clean`, and `layout_match` modes, returns JSON with a `tables` array. For `markdown` mode, returns `text/markdown`.",
        "operationId": "getJobResult",
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Extraction complete",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string" },
                    "mode": { "$ref": "#/components/schemas/ExtractionMode" },
                    "fileName": { "type": "string" },
                    "pageCount": { "type": "integer" },
                    "creditsConsumed": { "type": "integer" },
                    "completedAt": { "type": "string", "format": "date-time" },
                    "tables": { "type": "array", "items": { "$ref": "#/components/schemas/ExtractedTable" } },
                    "metadata": { "type": "object" }
                  }
                }
              },
              "text/markdown": { "schema": { "type": "string" } }
            }
          },
          "202": { "description": "Job not yet complete — keep polling" }
        }
      }
    },
    "/api/v1/account/status": {
      "get": {
        "summary": "Get account status and quota",
        "description": "Returns a machine-readable `status` code agents can act on, plus full quota breakdown. Poll this before submitting large batches to avoid mid-batch failures.",
        "operationId": "getAccountStatus",
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountStatus" } } } }
        }
      }
    },
    "/api/v1/account/overage": {
      "patch": {
        "summary": "Configure overage billing",
        "description": "Enable/disable overage billing and optionally set a spending cap. When `overageEnabled` is true, jobs continue past your monthly quota and excess is billed via Stripe metered billing at your tier's per-page rate.",
        "operationId": "configureOverage",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["overageEnabled"],
                "properties": {
                  "overageEnabled": { "type": "boolean" },
                  "spendingCapCents": { "type": "integer", "nullable": true, "description": "Max overage spend per billing period in cents. null = unlimited." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "overageEnabled": { "type": "boolean" },
                    "spendingCapCents": { "type": "integer", "nullable": true },
                    "updatedAt": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "post": {
        "summary": "Register a webhook",
        "description": "Webhooks are signed with HMAC-SHA256. Verify: `sha256=HMAC-SHA256(secret, requestBody)` in the `X-TableForge-Signature` header. The secret is shown once on creation.",
        "operationId": "registerWebhook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "format": "uri", "description": "Must be HTTPS" },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": ["job.completed", "job.failed", "quota.approaching", "quota.exceeded", "overage.cap_reached"]
                    },
                    "description": "Omit to subscribe to all events"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Webhook" },
                    { "type": "object", "properties": { "secret": { "type": "string", "description": "HMAC signing secret — shown once" }, "note": { "type": "string" } } }
                  ]
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "List webhooks",
        "operationId": "listWebhooks",
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "webhooks": { "type": "array", "items": { "$ref": "#/components/schemas/Webhook" } } } } } } }
        }
      }
    },
    "/api/v1/webhooks/{webhookId}": {
      "delete": {
        "summary": "Delete a webhook",
        "operationId": "deleteWebhook",
        "parameters": [{ "name": "webhookId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "deleted": { "type": "boolean" }, "webhookId": { "type": "string" } } } } } },
          "404": { "description": "Webhook not found" }
        }
      }
    }
  }
}
