{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/arkrel-com/json-schema/arkrel-com-usage-schema.json",
  "title": "Usage",
  "x-generated": "2026-11-01",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/arkrel-com-openapi.yaml#/components/schemas/Usage",
  "type": "object",
  "required": [
    "period_start",
    "period_end",
    "pages_included",
    "pages_used",
    "pages_overage"
  ],
  "properties": {
    "period_start": {
      "type": "string",
      "format": "date-time"
    },
    "period_end": {
      "type": "string",
      "format": "date-time"
    },
    "pages_included": {
      "type": "integer"
    },
    "pages_included_used": {
      "type": "integer",
      "minimum": 0,
      "description": "Confirmed monthly included pages consumed this period."
    },
    "pages_catchup": {
      "type": "integer",
      "minimum": 0,
      "description": "Confirmed pages covered by Catch-Up this period."
    },
    "catchup_purchases": {
      "type": "array",
      "description": "Unrefunded Catch-Up purchases, including expired terms. Repurchases have independent clocks and page balances.",
      "items": {
        "type": "object",
        "required": [
          "tax_year",
          "purchased_at",
          "payment_settled_at",
          "expires_at"
        ],
        "properties": {
          "tax_year": {
            "type": "integer"
          },
          "purchased_at": {
            "type": "string",
            "format": "date-time"
          },
          "payment_settled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "catchup_pages_total": {
      "type": "integer",
      "minimum": 0,
      "description": "Catch-Up capacity: 3,000 pages per unexpired active purchase on this business. Each purchase lasts 12 months from settlement."
    },
    "catchup_pages_used": {
      "type": "integer",
      "minimum": 0,
      "description": "Pages consumed from currently active, unexpired Catch-Up purchases. Historical usage stays in the usage ledger."
    },
    "catchup_pages_remaining": {
      "type": "integer",
      "minimum": 0,
      "description": "Sum of unused pages on active, unexpired Catch-Up purchases. A repurchase starts with a fresh 3,000 pages. Pending pages reserve capacity separately; monthly included pages are used first, then Catch-Up, then packs."
    },
    "pages_used": {
      "type": "integer",
      "description": "Confirmed pages from documents that reached a terminal state, allocated to monthly included pages, the cumulative Catch-Up pool, pack credits, or Business metered overage."
    },
    "pages_pending": {
      "type": "integer",
      "description": "Estimated pages from documents still `queued`/`processing`,\nreserved against the plan's cap so a burst of uploads can't\noutrun quota enforcement before any of them finish processing.\nNot billed until confirmed; excluded from `pages_used`.\n"
    },
    "pages_overage": {
      "type": "integer",
      "description": "Confirmed metered overage pages this period for Business. Autopilot records no new overage and its usage is not reported to Stripe. Historical allocations remain unchanged. Summed directly from each\nbilled document's own recorded ledger split (`usage_events.\npages_overage`), never re-derived from `pages_used -\npages_included`, which would wrongly count pages a purchased\npage-pack credit already covered as billed overage.\n"
    },
    "pages_credits": {
      "type": "integer",
      "description": "Confirmed pages this period covered by a purchased page-pack credit, neither included nor overage, and never billed."
    },
    "appsumo_codes": {
      "type": "integer",
      "description": "Number of active AppSumo codes redeemed less than 12 months ago and not revoked. These codes on this organization, capped at 5. Redeemed at\nPOST /app/api/appsumo/redeem.\n"
    },
    "appsumo_pages_per_month": {
      "type": "integer",
      "description": "Pages/month contributed by AppSumo codes alone (300 per code),\nalready folded into `pages_included` above. These REPLACE the\nFree plan's 50 included pages while any code is active (no\nstacking with a paid plan in v1).\n"
    },
    "plan": {
      "type": "string"
    },
    "limits": {
      "$ref": "#/$defs/Limits"
    }
  },
  "$defs": {
    "Limits": {
      "type": "object",
      "description": "Enforced server-side limits (not aspirational copy). See\napi/src/lib/ratelimit.ts and api/src/lib/upload.ts for the\nimplementation these numbers must match.\n",
      "required": [
        "documents_write_requests_per_minute",
        "general_requests_per_minute",
        "auth_requests_per_minute",
        "mutation_requests_per_minute",
        "webhook_test_requests_per_minute",
        "max_upload_size_mb",
        "max_pages_per_document",
        "supported_mime_types"
      ],
      "properties": {
        "documents_write_requests_per_minute": {
          "type": "integer",
          "description": "Per-org limit on POST /v1/documents. Fixed (matches src/lib/ratelimit.ts's RATE_LIMITS.documentsWrite), typed as a plain integer, not a JSON Schema `const`, so the generated TS type (api/src/generated/openapiTypes.ts) stays a normal `number` a config-driven response can satisfy at compile time; the response contract test (Schemathesis) is what actually checks the live value."
        },
        "general_requests_per_minute": {
          "type": "integer",
          "description": "Per-org limit on all other /v1/* endpoints. Fixed, see above."
        },
        "auth_requests_per_minute": {
          "type": "integer",
          "description": "Per-IP limit on every dashboard sign-in/sign-up/2FA/passkey call (/app/api/auth/*). Fixed, see above."
        },
        "mutation_requests_per_minute": {
          "type": "integer",
          "description": "Per-org limit on every other state-changing endpoint: API keys,\nwebhooks (create/delete), schemas (create/update/delete), org\nsettings/invites/member removal, switching the active org, and\nthe session-cookie (/app/api/documents) twin of document\nupload/delete. Fixed, see documents_write_requests_per_minute.\n"
        },
        "webhook_test_requests_per_minute": {
          "type": "integer",
          "description": "Per-org limit on POST /app/api/webhooks/{id}/test, tighter than\nthe general mutation limit because each call makes the Worker\nitself issue an outbound HTTP request to an org-supplied URL.\nFixed, see documents_write_requests_per_minute.\n"
        },
        "max_upload_size_mb": {
          "type": "integer",
          "description": "Fixed, see documents_write_requests_per_minute."
        },
        "max_pages_per_document": {
          "type": "integer",
          "const": 500
        },
        "max_image_megapixels": {
          "type": "integer",
          "const": 50,
          "description": "Maximum decoded pixel count (width x height, in megapixels) for\na single frame of a PNG, WebP, or TIFF upload. These formats\nhave no safe reduced-resolution decode path, so a frame over\nthis ceiling is rejected outright with `IMAGE_DIMENSIONS_\nTOO_LARGE` (413). Resize it, or send it as a JPEG or PDF\ninstead (see `max_jpeg_megapixels`).\n"
        },
        "max_jpeg_megapixels": {
          "type": "integer",
          "const": 3200,
          "description": "Maximum decoded pixel count for a single JPEG frame, far\nhigher than max_image_megapixels because JPEG supports a\nscaled (\"draft mode\") decode at 1/2, 1/4, or 1/8 resolution.\nAn ordinary 108MP or 200MP phone-camera photo is accepted and\ndecoded at a reduced resolution rather than rejected. A JPEG\nwhose dimensions still exceed this ceiling even at 1/8\nresolution is rejected with `IMAGE_DIMENSIONS_TOO_LARGE` (413).\n"
        },
        "supported_mime_types": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    }
  }
}