{
  "openapi": "3.0.0",
  "paths": {
    "/api/v1/credits/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Get credit balance (API key)",
        "description": "Returns the workspace credit balance and the current credit cost per invoice upload.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Current balance and per-invoice cost",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditBalanceResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/credits/packages": {
      "get": {
        "operationId": "listPackages",
        "summary": "List credit packages for top-up (API key)",
        "description": "Returns active packages you can purchase via `POST /api/v1/credits/checkout`. Prices use the platform rate (credits × price per credit).",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Active credit packages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CreditPackagePublicDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/credits/checkout": {
      "post": {
        "operationId": "checkout",
        "summary": "Start Stripe Checkout to buy credits (API key)",
        "description": "Creates a Stripe Checkout session for the given package. Open `url` in a browser to pay, then call `POST /api/v1/credits/checkout/confirm` with `sessionId` (or wait for the Stripe webhook). Success/cancel redirect URLs are the same as Settings → Billing in the web app.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCheckoutDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSessionResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Package unavailable or invalid packageId",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Package or tenant not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "503": {
            "description": "Stripe is not configured. Add the secret key in Platform Admin → Settings → Integrations, or set STRIPE_SECRET_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/credits/checkout/confirm": {
      "post": {
        "operationId": "confirmCheckout",
        "summary": "Confirm a paid Checkout session (API key)",
        "description": "After the customer pays at Stripe, call this with the Checkout `sessionId` to credit the workspace immediately. Idempotent — safe to retry. The Stripe webhook also fulfills the same purchase.",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmCheckoutDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment confirmed and balance returned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmCheckoutResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid session id or payment not completed yet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Checkout session not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "503": {
            "description": "Stripe is not configured. Add the secret key in Platform Admin → Settings → Integrations, or set STRIPE_SECRET_KEY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/invoices/upload/init": {
      "post": {
        "operationId": "prepareUpload",
        "summary": "Prepare an invoice upload slot (API key)",
        "description": "Creates a pending invoice and returns `invoiceId`. Attach the file with `POST /api/v1/invoices/upload/:invoiceId/file`. Credits are not deducted until the file is attached.",
        "parameters": [],
        "responses": {
          "202": {
            "description": "Upload slot created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PrepareUploadResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/invoices/upload/{invoiceId}/file": {
      "post": {
        "operationId": "attachFile",
        "summary": "Attach file to a prepared invoice (API key)",
        "description": "Persists the file, debits credits, and enqueues extraction. Returns before OCR/LLM completes. Poll `statusUrl` (`GET /api/v1/invoices/:id`) until `ready` is true, then read `data`. If the extraction queue is down, credits are refunded and the call returns 503.",
        "parameters": [
          {
            "name": "invoiceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vendorId",
            "required": false,
            "in": "query",
            "description": "Optional vendor UUID (alternative to form field)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Invoice PDF or image (JPG, JPEG, PNG, PDF)"
                  },
                  "vendorId": {
                    "type": "string",
                    "description": "Optional vendor UUID to prefer Tier 1 template extraction"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "File stored and extraction queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadAcceptedResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Unsupported file type or file already attached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Invoice slot not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "503": {
            "description": "Extraction queue unavailable (credits refunded)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/invoices/upload": {
      "post": {
        "operationId": "uploadSingle",
        "summary": "Upload a single invoice (API key, one-step)",
        "description": "Creates the invoice, stores the file, debits credits, and enqueues extraction in one call. Prefer init + attach for clearer client progress when uploading large files. Poll `statusUrl` until `ready` is true, then read `data` for extracted JSON. If the extraction queue is down, credits are refunded and the call returns 503.",
        "parameters": [
          {
            "name": "vendorId",
            "required": false,
            "in": "query",
            "description": "Optional vendor UUID (alternative to form field)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Invoice PDF or image (JPG, JPEG, PNG, PDF)"
                  },
                  "vendorId": {
                    "type": "string",
                    "description": "Optional vendor UUID to prefer Tier 1 template extraction"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Upload accepted; extraction queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadAcceptedResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Unsupported file type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "402": {
            "description": "Insufficient credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Invoice slot not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "503": {
            "description": "Extraction queue unavailable (credits refunded)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    },
    "/api/v1/invoices/{id}": {
      "get": {
        "operationId": "getInvoice",
        "summary": "Get extracted invoice JSON (API key)",
        "description": "Poll after upload. While `ready` is false, extraction is still running and `data` is null. When `ready` is true, `data` is the structured invoice JSON (human corrections included).",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Invoice id returned from upload",
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Processing status and extracted invoice JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceJsonResponseDto"
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Invoice not found for this API key tenant",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorResponseDto"
                }
              }
            }
          }
        },
        "tags": [
          "programmatic"
        ],
        "security": [
          {
            "api-key": []
          }
        ]
      }
    }
  },
  "info": {
    "title": "SnapIt Programmatic API",
    "description": "Public API-key endpoints for invoice upload, extracted JSON, credit balance, and top-up. Authenticate with `Authorization: Bearer sk_live_…`. Full operator Swagger (including admin/JWT routes) remains at `/api/docs` on the API host.",
    "version": "1.0",
    "contact": {}
  },
  "tags": [
    {
      "name": "programmatic",
      "description": "API-key authenticated upload, invoice JSON, balance, and credit top-up"
    }
  ],
  "servers": [
    {
      "url": "https://api.trysnapit.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local development"
    }
  ],
  "components": {
    "securitySchemes": {
      "api-key": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Bearer sk_live_… (include the Bearer prefix)",
        "x-default": "Bearer sk_live_…"
      }
    },
    "schemas": {
      "ApiErrorResponseDto": {
        "type": "object",
        "properties": {
          "statusCode": {
            "type": "number",
            "example": 401
          },
          "message": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "example": "Invalid or missing API key"
          },
          "error": {
            "type": "string",
            "example": "Unauthorized"
          }
        },
        "required": [
          "statusCode",
          "message"
        ]
      },
      "CreditBalanceResponseDto": {
        "type": "object",
        "properties": {
          "balance": {
            "type": "number",
            "example": 42,
            "description": "Remaining credits for the tenant"
          },
          "creditCostPerInvoice": {
            "type": "number",
            "example": 1,
            "description": "Credits deducted when an invoice upload is accepted (before OCR completes)"
          }
        },
        "required": [
          "balance",
          "creditCostPerInvoice"
        ]
      },
      "CreditPackagePublicDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Starter"
          },
          "credits": {
            "type": "number",
            "example": 100,
            "description": "Credits granted when this package is purchased"
          },
          "priceCents": {
            "type": "number",
            "example": 1000,
            "description": "Price in the smallest currency unit (e.g. cents)"
          },
          "currency": {
            "type": "string",
            "example": "usd"
          }
        },
        "required": [
          "id",
          "name",
          "credits",
          "priceCents",
          "currency"
        ]
      },
      "CreateCheckoutDto": {
        "type": "object",
        "properties": {
          "packageId": {
            "type": "string",
            "description": "Credit package UUID to purchase"
          }
        },
        "required": [
          "packageId"
        ]
      },
      "CheckoutSessionResponseDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "example": "https://checkout.stripe.com/c/pay/cs_test_…",
            "description": "Open this URL in a browser to complete payment"
          },
          "sessionId": {
            "type": "string",
            "example": "cs_test_a1b2c3",
            "description": "Pass to POST /api/v1/credits/checkout/confirm after payment"
          }
        },
        "required": [
          "url",
          "sessionId"
        ]
      },
      "ConfirmCheckoutDto": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "Stripe Checkout Session id from the success redirect",
            "example": "cs_test_a1b2c3"
          }
        },
        "required": [
          "sessionId"
        ]
      },
      "ConfirmCheckoutResponseDto": {
        "type": "object",
        "properties": {
          "applied": {
            "type": "boolean",
            "description": "True when credits were added by this confirm call; false if already applied"
          },
          "credits": {
            "type": "number",
            "example": 100,
            "description": "Credits on the purchased package"
          },
          "balance": {
            "type": "number",
            "example": 142,
            "description": "Workspace balance after confirm"
          },
          "creditCostPerInvoice": {
            "type": "number",
            "example": 1
          }
        },
        "required": [
          "applied",
          "credits",
          "balance",
          "creditCostPerInvoice"
        ]
      },
      "PrepareUploadResponseDto": {
        "type": "object",
        "properties": {
          "invoiceId": {
            "type": "string",
            "format": "uuid",
            "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          },
          "processingStatus": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "completed",
              "failed",
              "needs_review"
            ],
            "example": "queued",
            "description": "Always queued until a file is attached"
          }
        },
        "required": [
          "invoiceId",
          "processingStatus"
        ]
      },
      "ProgrammaticInvoiceSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "extracted",
              "reviewed",
              "approved",
              "rejected",
              "duplicate"
            ],
            "example": "pending"
          },
          "processingStatus": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "completed",
              "failed",
              "needs_review"
            ],
            "example": "queued"
          },
          "tenantId": {
            "type": "string",
            "format": "uuid"
          },
          "vendorId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "invoiceNumber": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "processingStatus",
          "createdAt",
          "updatedAt"
        ]
      },
      "UploadAcceptedResponseDto": {
        "type": "object",
        "properties": {
          "invoice": {
            "$ref": "#/components/schemas/ProgrammaticInvoiceSummaryDto"
          },
          "fileId": {
            "type": "string",
            "format": "uuid",
            "description": "Stored file id; download via JWT /api/files/:fileId in the web app"
          },
          "statusUrl": {
            "type": "string",
            "example": "/api/v1/invoices/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "description": "Relative path — prepend your API host (e.g. https://api.trysnapit.com). Poll until `ready` is true, then read `data` for extracted JSON."
          }
        },
        "required": [
          "invoice",
          "fileId",
          "statusUrl"
        ]
      },
      "ExtractedLineItemDto": {
        "type": "object",
        "properties": {
          "line_no": {
            "type": "number",
            "nullable": true,
            "example": 1
          },
          "description": {
            "type": "string",
            "example": "Coca Cola 330ml x24"
          },
          "sku": {
            "type": "string",
            "nullable": true,
            "example": "CC-330-24"
          },
          "quantity": {
            "type": "number",
            "nullable": true,
            "example": 2
          },
          "unit": {
            "type": "string",
            "nullable": true,
            "example": "ctn"
          },
          "unit_price": {
            "type": "number",
            "nullable": true,
            "example": 50
          },
          "discount": {
            "type": "number",
            "nullable": true,
            "example": 0
          },
          "tax": {
            "type": "number",
            "nullable": true,
            "example": 6
          },
          "line_total": {
            "type": "number",
            "nullable": true,
            "example": 100
          }
        },
        "required": [
          "description"
        ]
      },
      "ExtractedInvoiceDataDto": {
        "type": "object",
        "properties": {
          "invoice_number": {
            "type": "string",
            "nullable": true,
            "example": "INV-1042"
          },
          "invoice_date": {
            "type": "string",
            "nullable": true,
            "example": "2026-09-01"
          },
          "due_date": {
            "type": "string",
            "nullable": true,
            "example": "2026-09-30"
          },
          "vendor_name": {
            "type": "string",
            "nullable": true,
            "example": "Acme Supplies"
          },
          "vendor_address": {
            "type": "string",
            "nullable": true
          },
          "vendor_tax_id": {
            "type": "string",
            "nullable": true
          },
          "vendor_email": {
            "type": "string",
            "nullable": true
          },
          "customer_name": {
            "type": "string",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "example": "MYR"
          },
          "payment_terms": {
            "type": "string",
            "nullable": true
          },
          "subtotal": {
            "type": "number",
            "nullable": true,
            "example": 100
          },
          "discount": {
            "type": "number",
            "nullable": true,
            "example": 0
          },
          "tax_amount": {
            "type": "number",
            "nullable": true,
            "example": 6
          },
          "service_charge": {
            "type": "number",
            "nullable": true,
            "example": 0
          },
          "shipping_cost": {
            "type": "number",
            "nullable": true,
            "example": 0
          },
          "total_amount": {
            "type": "number",
            "nullable": true,
            "example": 106
          },
          "paid_amount": {
            "type": "number",
            "nullable": true,
            "example": 0
          },
          "balance_amount": {
            "type": "number",
            "nullable": true,
            "example": 106
          },
          "line_items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExtractedLineItemDto"
            }
          },
          "raw_text": {
            "type": "string",
            "nullable": true,
            "description": "OCR raw text when available"
          }
        }
      },
      "InvoiceJsonVendorDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "vendorName": {
            "type": "string",
            "example": "Acme Supplies"
          }
        },
        "required": [
          "id",
          "vendorName"
        ]
      },
      "InvoiceJsonResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "processingStatus": {
            "type": "string",
            "enum": [
              "queued",
              "processing",
              "completed",
              "failed",
              "needs_review"
            ],
            "example": "completed"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "extracted",
              "reviewed",
              "approved",
              "rejected",
              "duplicate"
            ],
            "example": "extracted"
          },
          "direction": {
            "type": "string",
            "enum": [
              "payable",
              "receivable"
            ],
            "example": "payable",
            "description": "Payable (vendor bill) or receivable (customer invoice)."
          },
          "ready": {
            "type": "boolean",
            "example": true,
            "description": "True when extraction has finished (completed, needs_review, or failed). Keep polling while false."
          },
          "data": {
            "nullable": true,
            "description": "Extracted invoice fields. Null while queued/processing with no result yet.",
            "allOf": [
              {
                "$ref": "#/components/schemas/ExtractedInvoiceDataDto"
              }
            ]
          },
          "ocrConfidence": {
            "type": "number",
            "nullable": true,
            "example": 92.5
          },
          "extractionTier": {
            "type": "number",
            "nullable": true,
            "example": 2,
            "description": "Platform admin only. Omitted from tenant responses."
          },
          "extractionProvider": {
            "type": "string",
            "nullable": true,
            "example": "mistral",
            "description": "Platform admin only. Omitted from tenant responses."
          },
          "routingReason": {
            "type": "string",
            "nullable": true,
            "description": "Platform admin only. Omitted from tenant responses."
          },
          "validationErrors": {
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "processingError": {
            "type": "string",
            "nullable": true
          },
          "isDuplicate": {
            "type": "boolean",
            "example": false
          },
          "vendor": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/InvoiceJsonVendorDto"
              }
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "processingStatus",
          "status",
          "direction",
          "ready",
          "validationErrors",
          "isDuplicate",
          "createdAt",
          "updatedAt"
        ]
      }
    }
  },
  "security": [
    {
      "api-key": []
    }
  ]
}
