openapi: 3.1.0
info:
  title: PDF Tagger API
  version: "1.0.0"
  description: >
    Tag PDFs for PDF/UA accessibility from your own code. Authenticate with an API key, upload a
    PDF, submit a tagging job, then poll the job or receive a signed webhook when it finishes. Jobs
    draw from the same credit balance as the web app (1 credit per page).
servers:
  - url: https://api.pdf-tagger.com
    description: Production
security:
  - apiKey: []

tags:
  - name: Account
    description: Your account and credit balance.
  - name: Jobs
    description: Upload a PDF, submit a tagging job, and download the result.
  - name: Webhooks
    description: >
      Signed job events delivered to your server. Register and manage endpoints in the dashboard
      (the Developers page); this section documents the event you receive and how to verify it.

paths:
  /v1/me:
    get:
      summary: Get account
      description: The authenticated account, its plan, and its current credit balance.
      tags: [Account]
      operationId: getMe
      responses:
        "200":
          description: Account info
          content:
            application/json:
              schema:
                type: object
                properties:
                  account_id: { type: string }
                  plan: { type: [string, "null"] }
                  subscription_status: { type: [string, "null"] }
                  credits_available: { type: integer }
                  request_id: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/uploads:
    post:
      summary: Create upload
      tags: [Jobs]
      description: >
        Returns a presigned POST. Upload the PDF straight to storage as multipart/form-data,
        including every field in `fields` plus the file, then pass `key` to POST /v1/jobs.
      operationId: createUpload
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                filename: { type: string, description: The original filename (optional). }
      responses:
        "201":
          description: Presigned upload
          content:
            application/json:
              schema:
                type: object
                properties:
                  upload_url: { type: string }
                  fields: { type: object, additionalProperties: { type: string } }
                  key: { type: string }
                  request_id: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/jobs:
    post:
      summary: Submit job
      description: Submit a tagging job for an uploaded PDF. Charges one credit per page.
      tags: [Jobs]
      operationId: createJob
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string }
          description: A repeat with the same key returns the first job and never double-charges.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [key]
              properties:
                key: { type: string, description: The key from POST /v1/uploads. }
                filename: { type: string }
                options:
                  type: object
                  properties:
                    ocr: { type: boolean, default: false, description: Force OCR on image-only pages. }
                    embed_fonts: { type: boolean, default: false, description: Embed a non-embedded font when safe. }
                  additionalProperties: false
      responses:
        "202":
          description: Job accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Job" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402":
          description: Not enough credits
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "409":
          description: Idempotency conflict or a job still being created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/jobs/{id}:
    get:
      summary: Get job
      description: A job's current status — processing, completed, or failed.
      tags: [Jobs]
      operationId: getJob
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Job
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Job" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/jobs/{id}/result:
    get:
      summary: Get result
      description: A short-lived download URL for the tagged PDF, once the job is completed.
      tags: [Jobs]
      operationId: getJobResult
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: A presigned download URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  result_url: { type: string }
                  expires_in: { type: integer, description: Seconds the URL is valid. }
                  request_id: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: The job is not completed yet
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }

webhooks:
  jobCompleted:
    post:
      summary: Job event
      tags: [Webhooks]
      description: >
        POSTed to every enabled endpoint. Verify the `X-PDFTagger-Signature` header before trusting
        the body: it is `t=<unixSeconds>,v1=<hmac>`, where the HMAC is SHA-256 over `<t>.<rawBody>`
        keyed by the endpoint secret. Reject a timestamp older than a few minutes (replay). Delivery
        is at least once — dedupe on `data.job.id`. The event carries no result URL; fetch it with
        GET /v1/jobs/{id}/result.
      parameters:
        - name: X-PDFTagger-Signature
          in: header
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                id: { type: string }
                type: { type: string, enum: [job.completed, job.failed] }
                created: { type: integer }
                data:
                  type: object
                  properties:
                    job: { $ref: "#/components/schemas/Job" }
      responses:
        "2XX": { description: Any 2xx acknowledges receipt. Non-2xx is retried with backoff. }

components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "Send your key as `Authorization: Bearer sk_live_…`."
  schemas:
    Job:
      type: object
      properties:
        id: { type: string }
        status: { type: string, enum: [processing, completed, failed] }
        filename: { type: [string, "null"] }
        credits_used: { type: integer }
        options: { type: [object, "null"] }
        created_at: { type: string, format: date-time }
        request_id: { type: string }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type: { type: string }
            message: { type: string }
            request_id: { type: string }
  responses:
    Unauthorized:
      description: Missing, invalid, or revoked API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No such resource for this account
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Rate limit exceeded (see the Retry-After header)
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until the window resets.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
