{
  "openapi": "3.0.1",
  "info": {
    "title": "Red Eagle CDR API",
    "description": "Content Disarm and Reconstruction: submit a document, get back a rebuilt, disarmed copy. Authenticate with OAuth2 client credentials (scope cdr.api) from RedEagle.Identity — the recommended default — or, for integration platforms (Zapier, Power Automate, n8n, Make) and quick tests, send an API key from the account portal as an X-Api-Key header.",
    "contact": {
      "name": "Red Eagle Tech Support",
      "url": "https://redeagle.tech/contact",
      "email": "support@redeagle.tech"
    },
    "license": {
      "name": "Red Eagle Tech License",
      "url": "https://redeagle.tech/terms"
    },
    "version": "v1"
  },
  "paths": {
    "/v1/account": {
      "get": {
        "tags": [
          "V1Account"
        ],
        "summary": "Get the account snapshot",
        "description": "Returns your organisation's current plan shape and live running values: the\r\nbilling period bounds, documents used and remaining this period, the prepaid\r\ncredit balance in pence and your rate limit. Always a fresh read — never a\r\ncached view — so it is safe to reconcile against.",
        "operationId": "GetAccount",
        "responses": {
          "200": {
            "description": "The account snapshot",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AccountResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountResponse"
                }
              }
            }
          },
          "404": {
            "description": "NotProvisioned — the organisation has no CDR account yet",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          },
          "401": {
            "description": "Missing, expired or invalid bearer token"
          }
        }
      }
    },
    "/v1/documents": {
      "post": {
        "tags": [
          "V1Documents"
        ],
        "summary": "Submit a document",
        "description": "Uploads a document for Content Disarm and Reconstruction. By default the\r\ndocument is sanitised asynchronously: poll the returned statusUrl until the\r\nenvelope reaches a terminal status, then fetch the cleaned file from\r\ndownloadUrl.\r\n            \r\n## Sync mode\r\n            \r\nAdd ?wait=true (or send Prefer: respond-sync) to hold the request open\r\nwhile the document is sanitised: if it reaches a terminal state within the\r\nwait budget (45 seconds by default; configurable server-side) the response\r\nis 200 with the full terminal envelope — downloadUrl in hand from one call.\r\nA document still in flight when the budget runs out degrades gracefully to\r\nthe normal 202 + poll contract; nothing is lost. Sync mode is identical\r\nbilling, enforcement and processing — recommended for files up to ~10 MB.\r\n            \r\nThe accepted response carries exact usage headers — X-CDR-Usage-Used,\r\nX-CDR-Usage-Allowance and X-CDR-Usage-Remaining on tier plans, or\r\nX-CDR-Credit-Balance-Pence (the post-charge balance) on pay-as-you-go — so\r\nevery accepted submission tells you where you stand.\r\n            \r\n## Supported file types\r\n            \r\nWord (doc, dot, docx, docm, dotx, dotm), Excel (xls, xlsx, xlsb, xlsm, xltx,\r\nxltm, xlam), PowerPoint (ppt, pps, pot, ppa, pptx, pptm, potx, potm, ppam,\r\nppsx, ppsm), PDF/FDF, rtf, OpenDocument (odt, ods, odp), svg, html/htm,\r\nimages (jpg, jpeg, png, gif, bmp, tif, tiff, webp) and archives (zip, gz).\r\nMaximum size 100 MB on this multipart route — larger files (up to your\r\nplan's ceiling) go via POST /v1/uploads and submit-by-reference.\r\n            \r\nSome formats are converted rather than rebuilt in place: svg, html/htm, ods\r\nand odp are returned as PDF, and gz as zip. A completed document that was\r\nconverted carries \"converted\" in its modifications and an outputFormat field,\r\nand its download is named with the new extension — do not assume the cleaned\r\nfile has the extension you submitted.\r\n            \r\nAn archive is accepted only when every file inside it is itself a supported\r\ntype. Size limits apply to the UNCOMPRESSED contents, so an archive that is\r\nsmall on the wire can still be rejected with file_too_large once expanded.",
        "operationId": "SubmitDocument",
        "parameters": [
          {
            "name": "wait",
            "in": "query",
            "description": "Hold the request open for the result (sync mode); defaults to false.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "waitSeconds",
            "in": "query",
            "description": "Cap the sync-mode wait at N seconds (clamped to the server budget; implies wait). For callers with their own step deadline — e.g. a 30-second automation-platform step budget.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "statusStyle",
            "in": "query",
            "description": "Omit normally. The literal 202 selects the 202-while-pending status contract for workflow engines that poll the Location header until a non-202 response: statusUrl then answers 202 + Location + Retry-After while in flight and 200 only at a terminal state.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Optional retry safety (Stripe semantics): send any unique value and retries of the same request replay the original response for 24 hours instead of double-submitting. Replays carry Idempotency-Replayed: true; a concurrent duplicate gets 409 IdempotencyKeyInFlight.",
            "schema": {
              "maxLength": 200,
              "type": "string"
            }
          },
          {
            "name": "filename",
            "in": "query",
            "description": "The document's file name — required when (and only when) the body is application/octet-stream, which carries no name of its own. The raw lane exists for platforms whose binary parameters map onto a bare body (Power Platform connectors) and for the simplest possible curl; it also requires Content-Length. deleteAfterDownload is accepted as a query parameter on this lane (the multipart form field cannot ride a raw body).",
            "schema": {
              "type": "string"
            },
            "example": "report.docx"
          },
          {
            "name": "deleteAfterDownload",
            "in": "query",
            "description": "Raw-binary lane only: the octet-stream equivalent of the multipart deleteAfterDownload form field.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "requestBody": {
          "description": "Either the file itself (multipart/form-data, up to 100 MB) or, as application/json, the uploadId of a staged upload from POST /v1/uploads (submit-by-reference — for files up to your plan's upload ceiling). Both lanes run the identical pipeline and return the identical envelope.",
          "content": {
            "multipart/form-data": {
              "schema": {
                "required": [
                  "file"
                ],
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "description": "File to be processed by the CDR service",
                    "format": "binary"
                  },
                  "deleteAfterDownload": {
                    "type": "boolean"
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitByReferenceRequest"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sync mode only — the document reached a terminal state within the wait budget; the envelope is final",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              }
            }
          },
          "202": {
            "description": "Accepted — the envelope carries the id and statusUrl to poll (sync mode degrades here when the budget runs out)",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              }
            }
          },
          "400": {
            "description": "NoFileProvided or UnsupportedFileType",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "402": {
            "description": "InsufficientCredit — the prepaid balance cannot cover the per-document price",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "CustomerSuspended or NotProvisioned",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "413": {
            "description": "FileTooLargeForPlan — the body carries maxSizeBytes and the lane that would accept the file",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "QuotaExceeded (Retry-After gives the period end) or RateLimited",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          },
          "401": {
            "description": "Missing, expired or invalid bearer token",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "UploadNotFound (submit-by-reference only) — unknown, expired, or not your upload"
          },
          "409": {
            "description": "UploadAlreadySubmitted (submit-by-reference only) — an upload is one-shot; or IdempotencyKeyInFlight"
          },
          "411": {
            "description": "LengthRequired (raw-binary lane only) — octet-stream submissions must declare Content-Length"
          },
          "415": {
            "description": "UnsupportedContentEncoding (raw-binary lane only) — send raw bytes; Content-Encoding is not decoded"
          }
        }
      }
    },
    "/v1/documents/{id}": {
      "get": {
        "tags": [
          "V1Documents"
        ],
        "summary": "Get a document's status",
        "description": "Returns the document envelope. Statuses: pending and processing (poll again\r\nafter pollAfterSeconds), completed (outcome, modifications and a 24-hour\r\ndownloadUrl), rejected (reason — e.g. threat_detected with threatNames — the\r\nfile's own properties stopped it) and failed (reason — timeout or\r\ninternal_error — automatically credited back).\r\n            \r\nStatus reads have their own generous rate-limit partition, so polling never\r\nconsumes your submission budget.\r\n            \r\n## statusStyle=202\r\n            \r\nMade for workflow engines (Power Automate, Logic Apps) that natively poll a\r\n202's Location header until the first non-202 response: with\r\n?statusStyle=202 this route answers 202 + Location (itself) + Retry-After\r\nwhile the document is in flight — the envelope still rides in the body —\r\nand 200 with the terminal envelope once it is completed, rejected or\r\nfailed. Submit with ?statusStyle=202 and the returned statusUrl/Location\r\ncarries the opt-in for you, so the engine's polling needs no flow logic at\r\nall. Everything else about the envelope is identical; without the\r\nparameter this route always answers 200, as ever.",
        "operationId": "GetDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The document id returned at submission.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "statusStyle",
            "in": "query",
            "description": "Omit normally. The literal 202 selects the 202-while-pending contract described above.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The document envelope (with statusStyle=202: only once terminal)",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              }
            }
          },
          "202": {
            "description": "statusStyle=202 only — still in flight; follow Location after Retry-After seconds",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentResponse"
                }
              }
            }
          },
          "404": {
            "description": "DocumentNotFound — no such document under your organisation",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          },
          "401": {
            "description": "Missing, expired or invalid bearer token"
          }
        }
      },
      "delete": {
        "tags": [
          "V1Documents"
        ],
        "summary": "Delete the cleaned file",
        "description": "Deletes a completed document's cleaned file ahead of the standard retention\r\nwindow (plan 05d D7). The document's status, outcome and metadata remain\r\nreadable; only the file is removed, permanently. Idempotent — deleting an\r\nalready-deleted document succeeds. A document still in flight cannot be\r\ndeleted (409): wait for its terminal state.",
        "operationId": "DeleteDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The document id returned at submission.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "The cleaned file is gone (or there was never one to remove)"
          },
          "404": {
            "description": "DocumentNotFound — no such document under your organisation",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "DocumentNotDeletable — the document is still in flight",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          },
          "401": {
            "description": "Missing, expired or invalid bearer token"
          }
        }
      }
    },
    "/v1/documents/{id}/download": {
      "get": {
        "tags": [
          "V1Documents"
        ],
        "summary": "Download the cleaned file",
        "description": "Redirects (302) to the cleaned file's time-limited link while it is valid.\r\nLinks last 24 hours from completion; after that this route returns 404 and\r\nthe document must be submitted again. Do not send your bearer token to the\r\nredirect target — it is a pre-authorised storage link.\r\n            \r\nFor documents submitted with deleteAfterDownload, the first call here starts\r\nthe deletion grace clock; a deleted document returns 410.",
        "operationId": "DownloadDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "The document id returned at submission.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the cleaned file"
          },
          "404": {
            "description": "DocumentNotFound, or DownloadExpired once the link has lapsed",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "DocumentNotCompleted — the document extension carries its current envelope",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "410": {
            "description": "DocumentDeleted — removed by your DELETE or the delete-after-download sweep",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          },
          "401": {
            "description": "Missing, expired or invalid bearer token"
          }
        }
      }
    },
    "/v1/uploads": {
      "post": {
        "tags": [
          "V1Uploads"
        ],
        "summary": "Create an upload",
        "description": "Returns everything needed to stage a large file: a create+write-only link to\r\none server-chosen blob (PUT the raw file bytes to it with the required\r\nheaders), your plan's size ceiling, and the link's expiry. The file name is\r\nvalidated now so a doomed upload is refused before any bytes move.\r\n            \r\nOptionally send a Content-MD5 header with the PUT — storage then verifies\r\nthe received bytes, turning transfer corruption into an immediate, unbilled\r\n400 instead of a billed rejection at processing time.\r\n            \r\nCreating an upload is submission intent: it shares the submission rate-limit\r\nbudget and is refused up front when the account could not submit — but\r\nnothing meters or bills until submit-by-reference is accepted.",
        "operationId": "CreateUpload",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Optional retry safety (Stripe semantics): send any unique value and retries of the same request replay the original response for 24 hours instead of double-submitting. Replays carry Idempotency-Replayed: true; a concurrent duplicate gets 409 IdempotencyKeyInFlight.",
            "schema": {
              "maxLength": 200,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The file name you are going to upload.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The upload envelope — PUT the file to uploadUrl, then submit the uploadId",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResponse"
                }
              }
            }
          },
          "400": {
            "description": "UnsupportedFileType",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "402": {
            "description": "InsufficientCredit",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "CustomerSuspended or NotProvisioned",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "QuotaExceeded or RateLimited",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error"
          },
          "401": {
            "description": "Missing, expired or invalid bearer token",
            "headers": {
              "Idempotency-Replayed": {
                "description": "Present (true) when this response is a stored replay of the first request under this Idempotency-Key.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AccountResponse": {
        "required": [
          "organisationId",
          "organisationName",
          "planType",
          "status"
        ],
        "type": "object",
        "properties": {
          "organisationId": {
            "type": "string",
            "description": "The organisation the token belongs to.",
            "nullable": true
          },
          "organisationName": {
            "type": "string",
            "description": "The organisation's display name.",
            "nullable": true,
            "example": "Example Ltd"
          },
          "planType": {
            "type": "string",
            "description": "Plan category: tier, payg or internal.",
            "nullable": true,
            "example": "payg"
          },
          "status": {
            "type": "string",
            "description": "Account lifecycle state: active or suspended.",
            "nullable": true,
            "example": "active"
          },
          "monthlyAllowance": {
            "type": "integer",
            "description": "Documents included per billing period (tier plans; 0 for PAYG).",
            "format": "int64"
          },
          "periodStart": {
            "type": "string",
            "description": "Start of the current billing period (UTC).",
            "format": "date-time"
          },
          "periodEnd": {
            "type": "string",
            "description": "End of the current billing period (UTC, exclusive) — when the allowance resets.",
            "format": "date-time"
          },
          "periodUsedSubmissions": {
            "type": "integer",
            "description": "Billable documents accepted in the current period.",
            "format": "int64"
          },
          "periodRemainingSubmissions": {
            "type": "integer",
            "description": "Documents remaining in the current period's allowance (tier plans; floored at\r\nzero). Null for plans without an allowance.",
            "format": "int64",
            "nullable": true
          },
          "creditBalancePence": {
            "type": "integer",
            "description": "Prepaid credit balance in pence. Present for every plan — credit can be\r\nbought alongside a subscription and stays spendable if the plan ever lapses\r\nto pay-as-you-go, so a funded balance is real money on any plan type.",
            "format": "int64",
            "example": 4970
          },
          "pricePerDocumentPence": {
            "type": "integer",
            "description": "Price per accepted document in pence, spent from credit at submission time.\r\nPay-as-you-go plans only — on other plans credit is not burned per document,\r\nso the field is absent.",
            "format": "int32",
            "nullable": true,
            "example": 15
          },
          "rateLimitPerMinute": {
            "type": "integer",
            "description": "The plan's API rate limit (requests per minute).",
            "format": "int32",
            "example": 60
          },
          "maxFileSizeBytes": {
            "type": "integer",
            "description": "The multipart lane's file-size ceiling in bytes (every plan). Larger files\r\ngo via POST /v1/uploads.",
            "format": "int64",
            "example": 104857600
          },
          "maxUploadSizeBytes": {
            "type": "integer",
            "description": "Your plan's upload-lane ceiling in bytes.",
            "format": "int64",
            "example": 262144000
          }
        },
        "additionalProperties": false,
        "description": "The caller's own live entitlement snapshot (plan 05b §3, GET /v1/account) — the\r\nprogrammatic balance check every prepaid API is expected to have. Always a fresh\r\nrepository read, never the enforcement cache, so integrations can reconcile\r\nagainst it between submissions."
      },
      "DocumentResponse": {
        "required": [
          "id",
          "status",
          "statusUrl"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The document's request id.",
            "nullable": true,
            "example": "a1b2c3d4-e5f6-7890-abcd-1234567890ab"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle state: pending, processing, completed, rejected or failed.\r\nRejected means the file's own properties stopped it (billed); failed means\r\nthe service could not process it (automatically credited).",
            "nullable": true,
            "example": "completed"
          },
          "outcome": {
            "type": "string",
            "description": "Completion outcome: \"cleaned\" (the file was rebuilt and anything disallowed\r\nremoved) or \"clean\" (reserved). Completed status only.",
            "nullable": true,
            "example": "cleaned"
          },
          "modifications": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Coarse modification codes for a completed document: \"rebuilt\" always, plus\r\n\"converted\" when the document is returned in a different format from the one\r\nsubmitted. Completed status only.",
            "nullable": true
          },
          "outputFormat": {
            "type": "string",
            "description": "The format the cleaned document is returned in, when it differs from the one\r\nsubmitted — some formats are converted rather than rebuilt in place (an SVG\r\ncomes back as PDF). Present only alongside a \"converted\" modification, so an\r\nintegrator can branch on it without parsing the download name.",
            "nullable": true,
            "example": "pdf"
          },
          "reason": {
            "type": "string",
            "description": "Why the document was rejected (threat_detected, unsupported_file_type,\r\npassword_protected, file_too_large, invalid_file, policy_violation) or why\r\nprocessing failed (timeout, internal_error — these credit automatically).\r\nTerminal non-completed statuses only; absent on requests predating\r\nstructured outcomes.",
            "nullable": true,
            "example": "threat_detected"
          },
          "threatNames": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of the threats found (rejected/threat_detected only).",
            "nullable": true
          },
          "submittedAt": {
            "type": "string",
            "description": "When the document was submitted (UTC).",
            "format": "date-time"
          },
          "completedAt": {
            "type": "string",
            "description": "When processing reached its terminal state (UTC); null while in flight.",
            "format": "date-time",
            "nullable": true
          },
          "downloadUrl": {
            "type": "string",
            "description": "Time-limited download link for the cleaned file (completed only, while the\r\nlink is valid — see downloadExpiresAt).",
            "nullable": true
          },
          "downloadExpiresAt": {
            "type": "string",
            "description": "When the download link stops working (24 hours after completion).",
            "format": "date-time",
            "nullable": true
          },
          "statusUrl": {
            "type": "string",
            "description": "Where to poll for this document's status.",
            "nullable": true
          },
          "pollAfterSeconds": {
            "type": "integer",
            "description": "Suggested seconds to wait before the next status poll (pending/processing\r\nonly). Polling this often never affects your submission budget — status reads\r\nhave their own generous rate-limit partition.",
            "format": "int32",
            "nullable": true,
            "example": 2
          },
          "helpUrl": {
            "type": "string",
            "description": "Where to manage the account when something needs attention (terminal non-completed statuses).",
            "nullable": true
          },
          "deleteAfterDownload": {
            "type": "boolean",
            "description": "True when the document was submitted with the delete-after-download opt-in:\r\nthe envelope then never carries downloadUrl — fetch the cleaned file through\r\nthe document's download route, which starts the deletion grace clock.",
            "nullable": true
          },
          "deletedAt": {
            "type": "string",
            "description": "When the cleaned file was deleted (UTC) — by your DELETE call or the\r\ndelete-after-download sweep. The status and outcome remain readable; only\r\nthe file is gone.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The v1 document envelope (plan 05b §3) — the response shape every successful\r\n/v1/documents call returns (and the 409 body on a premature download), serialised\r\ncamelCase with null fields omitted so each status carries exactly its relevant\r\ndetail. Refusals use RedEagle.CdrService.Core.Models.SubmissionRefusalResponse; validation errors\r\nare plain-text 400s."
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": { }
      },
      "SubmitByReferenceRequest": {
        "required": [
          "uploadId"
        ],
        "type": "object",
        "properties": {
          "uploadId": {
            "maxLength": 64,
            "minLength": 1,
            "type": "string",
            "description": "The upload id returned by POST /v1/uploads.",
            "example": "u_9f2c4b1a0e8d47c6a3b5d9e1f2a4c6b8"
          },
          "deleteAfterDownload": {
            "type": "boolean",
            "description": "Opt in to early deletion: the cleaned file is removed shortly after your\r\nfirst download begins (a grace window covers retries), instead of waiting\r\nfor the standard retention window. Defaults to false. With this flag the\r\nenvelope carries no downloadUrl — fetch via the document's download route."
          }
        },
        "additionalProperties": false,
        "description": "JSON body for POST /v1/documents submit-by-reference (plan 05d §3.1): submits a\r\ncompleted staged upload into the identical processing pipeline the multipart lane\r\nuses."
      },
      "UploadCreateRequest": {
        "required": [
          "fileName"
        ],
        "type": "object",
        "properties": {
          "fileName": {
            "maxLength": 255,
            "minLength": 1,
            "type": "string",
            "description": "The name of the file you are going to upload, including its extension.",
            "example": "report.pdf"
          }
        },
        "additionalProperties": false,
        "description": "Body for POST /v1/uploads (plan 05d §3.1). The file name is validated against the\r\nsupported types at create time so a doomed upload is refused before any bytes move."
      },
      "UploadResponse": {
        "required": [
          "requiredHeaders",
          "uploadId",
          "uploadUrl"
        ],
        "type": "object",
        "properties": {
          "uploadId": {
            "type": "string",
            "description": "The upload id to submit by reference once the PUT completes.",
            "nullable": true,
            "example": "u_9f2c4b1a0e8d47c6a3b5d9e1f2a4c6b8"
          },
          "uploadUrl": {
            "type": "string",
            "description": "Where to PUT the file: a time-limited, single-blob, create+write-only link\r\nstraight to storage — the API is not in the data path, so the multipart size\r\nceiling does not apply. Optionally send Content-MD5 with the PUT: storage\r\nthen verifies the bytes it received, turning transfer corruption into an\r\nimmediate, unbilled 400 instead of a billed rejection.",
            "nullable": true
          },
          "method": {
            "type": "string",
            "description": "The HTTP method to use against uploadUrl.",
            "nullable": true,
            "example": "PUT"
          },
          "requiredHeaders": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Headers the PUT must carry.",
            "nullable": true
          },
          "maxSizeBytes": {
            "type": "integer",
            "description": "Your plan's upload-lane ceiling in bytes — a larger file is refused at\r\nsubmit-by-reference.",
            "format": "int64",
            "example": 262144000
          },
          "expiresAt": {
            "type": "string",
            "description": "When the upload link stops accepting the PUT (UTC).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "The 201 envelope for a created upload (plan 05d §3.1): everything the caller\r\nneeds to PUT the file directly to storage and then submit it by reference."
      }
    },
    "securitySchemes": {
      "Bearer": {
        "type": "http",
        "description": "OAuth2 client-credentials access token issued by RedEagle.Identity (scope cdr.api; admin endpoints require cdr.admin). The recommended default for server integrations.",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "ApiKey": {
        "type": "apiKey",
        "description": "Per-organisation API key from the account portal (recdr_…) — for integration platforms (Zapier, Power Automate, n8n, Make) and quick tests. Also accepted as an Authorization bearer value. Never valid on admin endpoints.",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "Bearer": [ ]
    },
    {
      "ApiKey": [ ]
    }
  ]
}