﻿{
  "info": {
    "name": "PDFox API",
    "description": "Complete REST API collection for PDFox.\n\n**Before you start**, set these collection variables:\n- `baseUrl` — your API base URL (shown on the Developer page in the app)\n- `apiKey` — a key created at Account → API Keys\n- `orgId` — your organisation UUID (Settings → Organisation)\n- `templateId` — UUID of a published template\n\nAfter generating a PDF, copy the returned `jobId` into the `jobId` variable to use the polling and download requests.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:3001",
      "type": "string",
      "description": "API base URL — copy from the Developer page in your PDFox account"
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string",
      "description": "Your API key — create one at Account → API Keys"
    },
    {
      "key": "orgId",
      "value": "",
      "type": "string",
      "description": "Your organisation UUID — find it at Settings → Organisation"
    },
    {
      "key": "templateId",
      "value": "",
      "type": "string",
      "description": "UUID of a published template — visible in the template editor top bar"
    },
    {
      "key": "jobId",
      "value": "",
      "type": "string",
      "description": "Job ID returned by POST /v1/generate — paste here to use the polling and download requests"
    }
  ],
  "item": [
    {
      "name": "PDF Generation",
      "description": "Queue PDF generation jobs from JSON data or a CSV file.",
      "item": [
        {
          "name": "Generate PDF from JSON",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"orgId\": \"{{orgId}}\",\n  \"templateId\": \"{{templateId}}\",\n  \"data\": {\n    \"customerName\": \"Alice Smith\",\n    \"invoiceNumber\": \"INV-0042\",\n    \"amount\": 1250.00,\n    \"items\": [\n      { \"description\": \"Consulting\", \"qty\": 5, \"price\": 250 }\n    ]\n  },\n  \"meta\": {\n    \"filename\": \"invoice-INV-0042\",\n    \"retentionSeconds\": 86400\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/v1/generate",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "generate"]
            },
            "description": "Queue a single PDF generation job from a JSON data object.\n\nReturns `202 Accepted` with a `jobId`. Copy that value into the `jobId` collection variable, then use **Get Job Status** to poll for completion.\n\n**Required scope:** `document:create`\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| orgId | UUID | ✅ | Must match the key's org |\n| templateId | UUID | ✅ | Template must be published |\n| data | object | ✅ | Becomes the Handlebars context |\n| webhookUrl | string (URL) | ❌ | POST callback on completion |\n| meta.filename | string | ❌ | Custom filename (without `.pdf`). Duplicate names get a numeric suffix (-1, -2, …) |\n| meta.retentionSeconds | integer | ❌ | Seconds to keep the file before auto-deletion. Cannot exceed your plan maximum. Common: 300 (5 min), 1800 (30 min), 3600 (1 hr), 86400 (1 day) |"
          },
          "response": [
            {
              "name": "202 Accepted",
              "status": "Accepted",
              "code": 202,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"jobId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": { "mode": "raw", "raw": "" },
                "url": { "raw": "{{baseUrl}}/v1/generate" }
              }
            }
          ]
        },
        {
          "name": "Generate PDFs from CSV",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "orgId",
                  "value": "{{orgId}}",
                  "type": "text"
                },
                {
                  "key": "templateId",
                  "value": "{{templateId}}",
                  "type": "text"
                },
                {
                  "key": "file",
                  "type": "file",
                  "src": "",
                  "description": "CSV file — column headers become template variable names. Max 500 rows, 10 MB."
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/v1/generate/csv",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "generate", "csv"]
            },
            "description": "Upload a CSV to generate one PDF per row. Column headers map directly to Handlebars template variables.\n\nReturns `202 Accepted` with an array of `jobIds` — one per CSV row.\n\n**Example CSV:**\n```\ncustomerName,invoiceNumber,amount\nAlice Smith,INV-001,1250.00\nBob Jones,INV-002,875.50\n```\n\n**Required scope:** `document:create`\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| orgId | UUID | ✅ | Form field |\n| templateId | UUID | ✅ | Form field |\n| file | File | ✅ | multipart/form-data, max 10 MB |\n| webhookUrl | string (URL) | ❌ | Called for each completed job |"
          },
          "response": [
            {
              "name": "202 Accepted",
              "status": "Accepted",
              "code": 202,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"jobIds\": [\n    \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n    \"b2c3d4e5-f6a7-8901-bcde-f12345678901\"\n  ]\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": { "mode": "formdata", "formdata": [] },
                "url": { "raw": "{{baseUrl}}/v1/generate/csv" }
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Jobs",
      "description": "List, poll, retry, and download completed PDF generation jobs.",
      "item": [
        {
          "name": "List Jobs",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/generate/jobs?orgId={{orgId}}&limit=50&offset=0",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "generate", "jobs"],
              "query": [
                {
                  "key": "orgId",
                  "value": "{{orgId}}",
                  "description": "Your organisation UUID (required)"
                },
                {
                  "key": "limit",
                  "value": "50",
                  "description": "Max results to return (1–100, default 50)"
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Pagination offset (default 0)"
                }
              ]
            },
            "description": "Returns a paginated list of generation jobs for your organisation, newest first.\n\n**Required scope:** `document:read`"
          }
        },
        {
          "name": "Get Job Status",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/generate/jobs/{{jobId}}?orgId={{orgId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "generate", "jobs", "{{jobId}}"],
              "query": [
                {
                  "key": "orgId",
                  "value": "{{orgId}}",
                  "description": "Your organisation UUID (required)"
                }
              ]
            },
            "description": "Poll this endpoint after submitting a generation job.\n\n**Status values:**\n- `queued` — waiting in the worker queue\n- `processing` — Playwright is rendering the PDF\n- `completed` — PDF is ready; call **Get Download URL** next\n- `failed` — see `errorMessage` for details\n- `cancelled` — job was cancelled\n\n**Required scope:** `document:read`"
          },
          "response": [
            {
              "name": "200 OK — completed",
              "status": "OK",
              "code": 200,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"status\": \"completed\",\n  \"templateId\": \"...\",\n  \"createdAt\": \"2026-05-15T10:00:00.000Z\",\n  \"completedAt\": \"2026-05-15T10:00:03.421Z\",\n  \"errorMessage\": null\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": { "raw": "{{baseUrl}}/v1/generate/jobs/{{jobId}}?orgId={{orgId}}" }
              }
            }
          ]
        },
        {
          "name": "Get Download URL",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/generate/jobs/{{jobId}}/download?orgId={{orgId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "generate", "jobs", "{{jobId}}", "download"],
              "query": [
                {
                  "key": "orgId",
                  "value": "{{orgId}}",
                  "description": "Your organisation UUID (required)"
                }
              ]
            },
            "description": "Returns a presigned S3 download URL for a completed job. The URL is valid for **1 hour** — no Authorization header needed to fetch the PDF itself.\n\nOnly works when `status === 'completed'`.\n\n**Required scope:** `document:read`"
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"url\": \"https://storage.example.com/documents/a1b2c3d4.pdf?X-Amz-Expires=3600&...\",\n  \"expiresIn\": 3600\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": { "raw": "{{baseUrl}}/v1/generate/jobs/{{jobId}}/download?orgId={{orgId}}" }
              }
            }
          ]
        },
        {
          "name": "Retry Failed Job",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "body": {
              "mode": "none"
            },
            "url": {
              "raw": "{{baseUrl}}/v1/generate/jobs/{{jobId}}/retry",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "generate", "jobs", "{{jobId}}", "retry"]
            },
            "description": "Re-queues a job that is in `failed` status. The original template version and input payload are reused — no data needs to be re-submitted.\n\nSet the `jobId` collection variable to the ID of the failed job before sending.\n\nQuota is checked at retry time. If your quota is exhausted since the original submission, the retry is rejected with `422 QUOTA_EXCEEDED`.\n\nReturns `422 JOB_NOT_FAILED` if the job is not in `failed` status.\n\n**Required scope:** `document:create`"
          },
          "response": [
            {
              "name": "202 Accepted",
              "status": "Accepted",
              "code": 202,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"jobId\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\"\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "body": { "mode": "none" },
                "url": { "raw": "{{baseUrl}}/v1/generate/jobs/{{jobId}}/retry" }
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Usage & Quota",
      "description": "Check your remaining PDF generation quota before submitting large batch jobs.",
      "item": [
        {
          "name": "Get Quota Summary",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/usage/quota?orgId={{orgId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "usage", "quota"],
              "query": [
                {
                  "key": "orgId",
                  "value": "{{orgId}}",
                  "description": "Your organisation UUID (required)"
                }
              ]
            },
            "description": "Returns a breakdown of included subscription quota and purchased quota packs.\n\nUse `effective.canGenerateNow` as a pre-flight check before large batch jobs — it mirrors the same check run by `POST /v1/generate`.\n\n**Required scope:** `usage:read`"
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"included\": {\n    \"dailyLimit\": 500,\n    \"dailyUsed\": 42,\n    \"monthlyLimit\": 10000,\n    \"monthlyUsed\": 312\n  },\n  \"purchasedPacks\": {\n    \"totalPurchasedRemaining\": 0,\n    \"packs\": []\n  },\n  \"effective\": {\n    \"canGenerateNow\": true,\n    \"remainingToday\": 458,\n    \"remainingThisMonth\": 9688\n  },\n  \"period\": {\n    \"dailyResetAt\": \"2026-05-16T00:00:00.000Z\",\n    \"monthlyResetAt\": \"2026-06-01T00:00:00.000Z\"\n  }\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": { "raw": "{{baseUrl}}/v1/usage/quota?orgId={{orgId}}" }
              }
            }
          ]
        },
        {
          "name": "Get Usage History",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{apiKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/usage/history?orgId={{orgId}}",
              "host": ["{{baseUrl}}"],
              "path": ["v1", "usage", "history"],
              "query": [
                {
                  "key": "orgId",
                  "value": "{{orgId}}",
                  "description": "Your organisation UUID (required)"
                }
              ]
            },
            "description": "Returns aggregated usage records for the organisation.\n\n**Required scope:** `usage:read`"
          }
        }
      ]
    },
    {
      "name": "Health",
      "description": "Database connectivity check — no authentication required.",
      "item": [
        {
          "name": "Health Check",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/health",
              "host": ["{{baseUrl}}"],
              "path": ["health"]
            },
            "description": "Returns `200 OK` with `{ \"status\": \"ok\" }` when the API and database are reachable. No authentication required.\n\nUseful for uptime monitoring."
          },
          "response": [
            {
              "name": "200 OK",
              "status": "OK",
              "code": 200,
              "header": [
                { "key": "Content-Type", "value": "application/json" }
              ],
              "body": "{\n  \"status\": \"ok\"\n}",
              "_postman_previewlanguage": "json",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": { "raw": "{{baseUrl}}/health" }
              }
            }
          ]
        }
      ]
    }
  ]
}
