{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/everypage-co/json-schema/everypage-co-file-settings-schema.json",
  "title": "FileSettings",
  "description": "All fields optional; omitted fields keep their current value.",
  "x-generated": "2026-11-07",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/everypage-openapi.json#/components/schemas/FileSettings",
  "type": "object",
  "properties": {
    "viewerMode": {
      "type": "string",
      "description": "How readers see the document. `standard` shows one page at a time, `scroll` every page in one continuous column, `flipbook` and `magazine` turn pages, `swipe` is a story-style card per page.",
      "enum": [
        "standard",
        "scroll",
        "flipbook",
        "swipe",
        "magazine"
      ]
    },
    "password": {
      "type": "string",
      "description": "Non-empty to protect (Basic+); empty string to clear."
    },
    "allowDownload": {
      "type": "boolean"
    },
    "requireEmail": {
      "type": "boolean",
      "description": "Gate viewing behind an email (Pro)."
    },
    "notifyOnView": {
      "type": "boolean",
      "description": "Email you on each read (Basic+)."
    },
    "deleteAt": {
      "type": "string",
      "format": "date-time",
      "description": "Fixed expiry. Uncapped for signed-in accounts on every plan (null = never expires); manage-token callers on an anonymous file are capped at 7 days. Set through a MANAGE TOKEN on an anonymous file, it also marks the expiry as deliberately chosen, so a Canva republish keeps it instead of re-basing to the 365-day default - a one-way mark (the token lane has no way to hand expiry back to the system)."
    },
    "neverExpire": {
      "type": "boolean",
      "description": "Permanent link, available to every signed-in plan (not through a manage token). Overrides deleteAt."
    },
    "viewLimit": {
      "type": "integer",
      "description": "Self-destruct after this many views (Basic+). 0 clears the limit; omit to keep. A burned link keeps its row and readership - un-burn it via content replacement."
    },
    "gateDomains": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Email-gate domain allowlist (Pro to set a non-empty list; [] clears on any plan)."
    },
    "gateFields": {
      "type": [
        "array",
        "null"
      ],
      "items": {
        "$ref": "#/$defs/GateField"
      },
      "description": "Lead-capture form definition shown instead of the plain email prompt (Pro to set; null or [] clears on any plan). A non-empty form forces requireEmail on."
    },
    "gateHeading": {
      "type": "string",
      "maxLength": 80,
      "description": "Heading on the gate card readers see before the document opens (Pro to set; \"\" clears back to the default wording on any plan; omit to keep). Trimmed; plain text, no line breaks. Shown only while requireEmail is on."
    },
    "gateMessage": {
      "type": "string",
      "maxLength": 280,
      "description": "Explanatory line under the gate heading (Pro to set; \"\" clears on any plan; omit to keep). Trimmed; plain text, no line breaks. Shown only while requireEmail is on."
    },
    "askReceipt": {
      "type": "boolean",
      "description": "Ask viewers to confirm receipt (Basic+ to enable)."
    },
    "showExpiry": {
      "type": "boolean",
      "description": "Show readers a live \"Expires in\" countdown next to Download (default true; any plan, either direction; omit to keep). False also withholds the expiry instant from the public share-link metadata. Moot while the link never expires."
    },
    "commentsEnabled": {
      "type": "boolean",
      "description": "Reader feedback notes (Pro to enable)."
    },
    "commentsAllowPublic": {
      "type": "boolean",
      "description": "Let commenters opt into public display."
    },
    "notifyOnComment": {
      "type": "boolean",
      "description": "Email you (throttled) when a note arrives."
    },
    "marksEnabled": {
      "type": "boolean",
      "description": "Proofing page marks (Pro to enable)."
    },
    "annotationsEnabled": {
      "type": "boolean",
      "description": "Proofing annotations (Pro to enable)."
    },
    "watermark": {
      "type": "boolean",
      "description": "Stamp viewer pages (Pro). Watermarked downloads are limited to files up to 100MB (400 above that)."
    },
    "slug": {
      "type": "string",
      "description": "Vanity link name on your custom domain (Pro): lowercase letters, digits, and hyphens; \"\" clears. 409 when another of your files already uses it."
    },
    "canvaUpdatesEnabled": {
      "type": "boolean",
      "description": "May a Canva-side republish of the bound design replace this file's bytes? Defaults true; only meaningful on files with a canvaDesignId. Un-gated on every plan in both directions - it is a safety control, not a feature."
    },
    "detachCanvaDesign": {
      "type": "boolean",
      "description": "True permanently severs the file from the Canva design that published it: no future publish of the design can update this link (the next publish creates a fresh one). One-way - there is no re-bind through settings - and un-gated on every plan. No-op on unbound files."
    },
    "viewerSettings": {
      "type": "object",
      "description": "The viewer appearance/behavior blob. Blocks: `background` (type solid|gradient|image with color/gradient preset/assetId, fit, blur 0-20, dim 0-80), `logo` (assetId, position tl|tr|bl|br, size s|m|l|xl, linkUrl, hideBadge), `page` (shadow 0-3, rounded 0-3, edges, coverAlone), `flip` (speedMs 200-1200, sound, rtl, layout adaptive|single|double), `swipe` (autoAdvance, intervalMs 3000-30000), `protect` (contextMenu/print/select Basic+; blurOnLeave Pro), `ga4Id` (Pro), `brand` (accentColor, toolbarTheme, toolbarColor #RRGGBB for the viewer toolbar strip - replaces toolbarTheme when set, badgePosition, headerColor #RRGGBB for the share page's top bar, headerLogo to show the `logo` asset there in place of the EveryPage name - dropped when no logo is set, headerButtons #RRGGBB to fill its Report and Download buttons; Pro), `cta` (label 1-40 chars, https url, style, color, position; Pro), `watermarkStyle` (opacity 0.05-0.5, density sparse|normal|dense; Pro), `gate` (the sign-in card readers see before the email or password gate: backgroundColor and buttonColor #RRGGBB, imageAssetId of a `gate-image` asset, imageStyle logo|banner; Pro), and `preview` (the link preview chat apps and social sites show for the share link: title 1-70 chars, required whenever the block is set, description up to 200 chars, imageAssetId of a `preview-image` asset composed into the 1200x630 card; shown even when the document is gated; never copied by templates; Pro). Any deviation from the defaults requires Basic+; unknown fields are dropped and out-of-range values clamped. Tier gates are re-applied at serve time, so a downgraded plan's Pro styling stops rendering for viewers immediately.\n"
    },
    "pageRange": {
      "allOf": [
        {
          "$ref": "#/$defs/PageRange"
        }
      ],
      "description": "Limit non-owner viewers to a page window: pages outside it are trimmed server-side before any bytes are served (the hidden pages never reach the browser; the public metadata advertises the active window as `pageRange {from,to,totalPages}` so viewers see an honest \"preview ends here\" card). Omit to keep the current range; `{\"from\":0,\"to\":0}` clears it; a valid pair (1 <= from <= to) sets it. SETTING a range requires the Pro plan and a file of at most 100MB (400 above that); CLEARING is never plan-gated. Enforcement deliberately survives a Pro downgrade: a lapsed subscription stops you setting new ranges but never widens access to pages an existing link already hides - clear the range to serve the full document again. You, the owner, always receive the full document. A per-variant `overrides.pageRange` beats this file-level range for viewers of that variant. Note pdfcpu's trim applies a reduced feature set to the served window (annotations and outlines are dropped).\n"
    }
  },
  "$defs": {
    "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)."
        }
      }
    }
  }
}