{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/everypage-co/json-schema/everypage-co-readership-report-schema.json",
  "title": "ReadershipReport",
  "description": "The owner's readership dashboard data. `file`, `tier` and `summary` are always present; the other sections follow the OWNER's plan (`tier`): Basic adds `effectivePages`, `viewsOverTime`, `funnel`, `referrers`, `browsers`, `sources`, `viewerModes`, `peakTimes`, `devices` and `countries`; Pro adds per-viewer `sessions` (with `variants` when the file has link variants) and, when configured, `hotspotTaps` and `ctaTaps`. Sections above the plan are omitted, not empty.",
  "x-generated": "2026-11-07",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/everypage-openapi.json#/components/schemas/ReadershipReport",
  "type": "object",
  "required": [
    "file",
    "tier",
    "summary"
  ],
  "properties": {
    "file": {
      "$ref": "#/$defs/File"
    },
    "tier": {
      "type": "string",
      "enum": [
        "free",
        "basic",
        "pro"
      ],
      "description": "The owner's plan, which decides which sections are present."
    },
    "summary": {
      "type": "object",
      "properties": {
        "totalViews": {
          "type": "integer"
        },
        "totalSessions": {
          "type": "integer"
        },
        "uniqueVisitors": {
          "type": "integer"
        },
        "uniqueCountries": {
          "type": "integer"
        },
        "avgTimeMs": {
          "type": "integer"
        },
        "downloads": {
          "type": "integer"
        }
      }
    },
    "variants": {
      "type": "array",
      "description": "Pro only, when the file has link variants: per-variant readership.",
      "items": {
        "type": "object",
        "properties": {
          "uuid": {
            "type": "string",
            "format": "uuid"
          },
          "label": {
            "type": "string",
            "description": "Omitted after GDPR redaction."
          },
          "sessions": {
            "type": "integer"
          },
          "avgTimeMs": {
            "type": "integer"
          },
          "revoked": {
            "type": "boolean"
          },
          "redacted": {
            "type": "boolean"
          }
        }
      }
    }
  },
  "additionalProperties": true,
  "$defs": {
    "File": {
      "type": "object",
      "properties": {
        "uuid": {
          "type": "string",
          "format": "uuid"
        },
        "shortId": {
          "type": "string",
          "description": "Short share-URL alias; absent on files created before short IDs."
        },
        "slug": {
          "type": "string",
          "description": "Vanity link name (Pro); absent unless set. Resolves only on the owner's custom domain."
        },
        "originalName": {
          "type": "string"
        },
        "size": {
          "type": "integer",
          "format": "int64",
          "description": "Bytes"
        },
        "mimeType": {
          "type": "string"
        },
        "createdAt": {
          "type": "string",
          "format": "date-time"
        },
        "updatedAt": {
          "type": "string",
          "format": "date-time"
        },
        "deleteAt": {
          "type": "string",
          "format": "date-time",
          "description": "Absent when the file never expires."
        },
        "viewCount": {
          "type": "integer"
        },
        "totalPages": {
          "type": "integer"
        },
        "viewerMode": {
          "type": "string",
          "description": "How readers see the document. `scroll` is a continuous vertical column of every page.",
          "enum": [
            "standard",
            "scroll",
            "flipbook",
            "swipe",
            "magazine"
          ]
        },
        "protected": {
          "type": "boolean",
          "description": "True if a password is set."
        },
        "allowDownload": {
          "type": "boolean"
        },
        "requireEmail": {
          "type": "boolean"
        },
        "notifyOnView": {
          "type": "boolean"
        },
        "commentsEnabled": {
          "type": "boolean",
          "description": "Reader feedback notes are on (Pro to enable)."
        },
        "commentsAllowPublic": {
          "type": "boolean",
          "description": "Commenters may opt into public display of their note."
        },
        "notifyOnComment": {
          "type": "boolean",
          "description": "The owner is emailed (throttled) when a note arrives."
        },
        "askReceipt": {
          "type": "boolean",
          "description": "Viewers are asked to confirm receipt (Basic+ to enable)."
        },
        "showExpiry": {
          "type": "boolean",
          "description": "Readers see a live \"Expires in\" countdown next to Download (default true, every plan). When false the public share-link metadata omits the expiry instant entirely."
        },
        "marksEnabled": {
          "type": "boolean",
          "description": "Proofing page marks are on (Pro to enable)."
        },
        "annotationsEnabled": {
          "type": "boolean",
          "description": "Proofing annotations are on (Pro to enable)."
        },
        "viewLimit": {
          "type": "integer",
          "description": "Self-destruct cap (Basic+). Absent when unlimited."
        },
        "viewsConsumed": {
          "type": "integer",
          "description": "Views consumed against `viewLimit` (always present, 0 when no limit)."
        },
        "burnedAt": {
          "type": "string",
          "format": "date-time",
          "description": "Present once the view-limit sweep destroyed the blob; the row and its readership survive, and content replacement un-burns it."
        },
        "underReview": {
          "type": "boolean",
          "description": "Present (true) while the document is paused for abuse review. The owner can still read it (this DTO, the content, readership); every write to it - settings, content, hotspots, assets, link variants, delete - answers 403 with detail \"This document is under review\" (code `forbidden`) until a person has reviewed it. Email support@everypage.co with the link to ask."
        },
        "pausedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the pause began. Present with `underReview`."
        },
        "reviewReasons": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "reports",
              "unsafe_link",
              "automatic_check"
            ]
          },
          "description": "Why the document is paused, as distinct kinds in this order - `reports` (readers reported it), `unsafe_link` (Google Web Risk lists a link in it), `automatic_check` (the hosted-page upload scan). Absent when the pause carries no open flag (a manual pause) and when not under review."
        },
        "gateDomains": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Email-gate domain allowlist (Pro). Absent when any domain is accepted."
        },
        "gateFields": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/GateField"
          },
          "description": "The stored lead-capture form definition (Pro). Absent when the gate is the plain email prompt."
        },
        "gateHeading": {
          "type": "string",
          "description": "The owner's heading for the gate card readers see before the document opens (Pro). Absent when the default wording applies."
        },
        "gateMessage": {
          "type": "string",
          "description": "The owner's explanatory line on the gate card (Pro). Absent when the default wording applies."
        },
        "watermark": {
          "type": "boolean",
          "description": "Viewer pages are stamped with the reader identity (Pro)."
        },
        "viewerSettings": {
          "type": "object",
          "additionalProperties": true,
          "description": "The stored appearance/behaviour blob exactly as saved (see the FileSettings.viewerSettings description); `{}` when never customised. Always present for the owner regardless of current plan."
        },
        "shareDomain": {
          "type": "string",
          "description": "The owner's verified Pro custom domain that share links and embeds are built on. Absent when links use everypage.co."
        },
        "folderId": {
          "type": "string",
          "format": "uuid",
          "description": "UUID of the folder the file is filed in. Absent when unfiled."
        },
        "anonShareable": {
          "type": "boolean",
          "description": "True for owner-less files that are public-by-link (browser anonymous uploads, Canva imports). Always false once claimed."
        },
        "revisions": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/Revision"
          },
          "description": "Replacement history - superseded content versions, newest first. Populated only by GET /api/v1/files/{uuid}; absent in the list and when the content was never replaced."
        },
        "pageRange": {
          "allOf": [
            {
              "$ref": "#/$defs/PageRange"
            }
          ],
          "description": "Present when a page range limits non-owner viewers (absent = the full document serves). Enforced regardless of your current plan (see the setting's description); owners always get the full document.\n"
        },
        "contentVersion": {
          "type": "integer",
          "description": "Counts in-place content replacements (1 = the original upload)."
        },
        "palette": {
          "$ref": "#/$defs/Palette"
        },
        "canvaDesignId": {
          "type": "string",
          "description": "The Canva design this file was published from. Absent on files with no design binding (including after a detach)."
        },
        "canvaUpdatesEnabled": {
          "type": "boolean",
          "description": "Whether a Canva-side republish of the bound design may replace this file's bytes (see the setting). Present only alongside canvaDesignId."
        }
      },
      "description": "The owner's view of a file (`FileResponse`). Optional fields are omitted when empty rather than sent as null. `revisions` is populated only by the single-file GET, never in the list."
    },
    "GateField": {
      "type": "object",
      "description": "One field of a lead-capture gate form. A form holds 1-10 fields, exactly one of type email (always first and required).",
      "required": [
        "key",
        "label",
        "type"
      ],
      "properties": {
        "key": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{0,39}$",
          "description": "Unique snake_case key; submitted values and captured responses are keyed by it."
        },
        "label": {
          "type": "string",
          "minLength": 1,
          "maxLength": 300
        },
        "type": {
          "type": "string",
          "enum": [
            "text",
            "email",
            "phone",
            "company",
            "select"
          ],
          "description": "`select` is a drop-down: the viewer picks exactly one of `options`. Text, phone and company values are capped at 500 characters."
        },
        "required": {
          "type": "boolean"
        },
        "options": {
          "type": "array",
          "minItems": 2,
          "maxItems": 20,
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "description": "The drop-down choices (type `select` only; unique). Omitted for every other type."
        }
      }
    },
    "PageRange": {
      "type": "object",
      "description": "A non-owner page window, 1-based and inclusive. `{\"from\":0,\"to\":0}` is the explicit \"no range\" value (clears the file-level range; as a variant override it serves the full document through that variant).\n",
      "properties": {
        "from": {
          "type": "integer",
          "minimum": 0,
          "description": "First shared page (1-based)."
        },
        "to": {
          "type": "integer",
          "minimum": 0,
          "maximum": 20000,
          "description": "Last shared page (inclusive; must be >= from)."
        }
      }
    },
    "Palette": {
      "type": "array",
      "description": "Colour palette extracted from the document at import time: hex swatches (\"#rrggbb\"), dominant colour first, at most 6. ADVISORY ONLY - extraction is best-effort (first-page embedded raster images, short time budget), so the field is simply absent for vector-only documents, for files uploaded through non-import paths, for files created before the feature, and whenever extraction fails or times out. Absence is normal, never an error; use the palette for theming (e.g. styling a share card), never for logic.\n",
      "items": {
        "type": "string"
      }
    },
    "Revision": {
      "type": "object",
      "description": "One superseded content version of a file (see `File.revisions`).",
      "properties": {
        "version": {
          "type": "integer",
          "description": "The content version this entry was (1 = the original upload)."
        },
        "originalName": {
          "type": "string"
        },
        "size": {
          "type": "integer",
          "format": "int64",
          "description": "Bytes"
        },
        "totalPages": {
          "type": "integer"
        },
        "createdAt": {
          "type": "string",
          "format": "date-time",
          "description": "When this version was uploaded."
        }
      }
    }
  }
}