{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/esyurl-fyi/json-schema/esyurl-fyi-short-link-schema.json",
  "title": "ShortLink",
  "x-generated": "2026-11-07",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/esyurl-fyi-openapi.json#/components/schemas/ShortLink",
  "type": "object",
  "required": [
    "linkId",
    "tenantId",
    "slug",
    "shortUrl",
    "tags",
    "metadata",
    "groupIds",
    "qrStyle",
    "status",
    "visitCount",
    "createdAt",
    "updatedAt"
  ],
  "properties": {
    "linkId": {
      "type": "string"
    },
    "tenantId": {
      "type": "string"
    },
    "slug": {
      "$ref": "#/$defs/Slug"
    },
    "targetUrl": {
      "type": "string",
      "format": "uri",
      "description": "Absent on link pages"
    },
    "page": {
      "$ref": "#/$defs/LinkPage"
    },
    "pageFooterPaid": {
      "type": "object",
      "properties": {
        "paymentKey": {
          "type": "string"
        },
        "at": {
          "type": "string",
          "format": "date-time"
        }
      },
      "description": "The page footer was paid off, for this link for good"
    },
    "warnings": {
      "type": "array",
      "description": "Create/update only: what reads badly on the page (low contrast, suspicious wording). The page was saved.",
      "items": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "enum": [
              "low_contrast",
              "suspicious_wording"
            ]
          },
          "path": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "ratio": {
            "type": "number"
          }
        }
      }
    },
    "shortUrl": {
      "type": "string",
      "format": "uri",
      "description": "`{base}/{slug}`; share this. QR images encode it with `?s=qr`."
    },
    "name": {
      "type": "string"
    },
    "tags": {
      "$ref": "#/$defs/Tags"
    },
    "metadata": {
      "$ref": "#/$defs/Metadata"
    },
    "groupIds": {
      "$ref": "#/$defs/GroupIds"
    },
    "qrStyle": {
      "$ref": "#/$defs/QrStyle"
    },
    "rules": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/RedirectRule"
      },
      "description": "Evaluated in order on each visit; first match wins, else targetUrl. [] on links without rules"
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "paused"
      ]
    },
    "visitCount": {
      "type": "integer",
      "description": "Lifetime human visits (qr + direct); bots excluded"
    },
    "lastVisitedAt": {
      "type": "string",
      "format": "date-time"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time"
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time"
    }
  },
  "$defs": {
    "GroupIds": {
      "type": "array",
      "maxItems": 20,
      "uniqueItems": true,
      "items": {
        "$ref": "#/$defs/Id"
      },
      "description": "A set: returned sorted; duplicates are dropped"
    },
    "HexColour": {
      "type": "string",
      "pattern": "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
    },
    "Id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9]{1,64}$"
    },
    "LinkPage": {
      "type": "object",
      "description": "A link page as stored: pictures are {imageId}, every item has its id, linkId items carry the slug they open.",
      "properties": {
        "title": {
          "type": "string"
        },
        "bio": {
          "type": "string"
        },
        "avatar": {
          "type": "object",
          "properties": {
            "imageId": {
              "type": "string"
            }
          }
        },
        "items": {
          "type": "array",
          "items": {
            "type": "object"
          }
        },
        "socials": {
          "type": "array",
          "items": {
            "type": "object"
          }
        },
        "theme": {
          "type": "object"
        },
        "language": {
          "type": "string"
        },
        "noindex": {
          "type": "boolean"
        },
        "footer": {
          "type": "boolean"
        }
      }
    },
    "Metadata": {
      "type": "object",
      "maxProperties": 20,
      "additionalProperties": {
        "type": "string",
        "maxLength": 512
      }
    },
    "QrStyle": {
      "type": "object",
      "additionalProperties": false,
      "description": "How the link's QR image looks (free). Styled codes (any of modules, eyeFrame, eyeBall, eyeColor, image) need dark on light at a contrast of at least 3:1. In requests, `image` carries a new picture as `data`, or keeps the current one by its `imageId`; responses carry `imageId`.",
      "properties": {
        "dark": {
          "$ref": "#/$defs/HexColour"
        },
        "light": {
          "$ref": "#/$defs/HexColour"
        },
        "errorCorrectionLevel": {
          "type": "string",
          "enum": [
            "L",
            "M",
            "Q",
            "H"
          ],
          "description": "Forced to H with a picture"
        },
        "margin": {
          "type": "integer",
          "minimum": 0,
          "maximum": 16
        },
        "modules": {
          "type": "string",
          "enum": [
            "square",
            "rounded",
            "dot",
            "row",
            "column"
          ],
          "description": "Shape of the data modules (default square)"
        },
        "eyeFrame": {
          "type": "string",
          "enum": [
            "square",
            "rounded",
            "circle"
          ],
          "description": "Outer ring of the three corner markers"
        },
        "eyeBall": {
          "type": "string",
          "enum": [
            "square",
            "rounded",
            "circle"
          ],
          "description": "Centre of the corner markers"
        },
        "eyeColor": {
          "$ref": "#/$defs/HexColour",
          "description": "Colour of the corner markers; defaults to dark"
        },
        "image": {
          "type": "object",
          "required": [
            "mode"
          ],
          "additionalProperties": false,
          "properties": {
            "mode": {
              "type": "string",
              "enum": [
                "logo",
                "halftone"
              ],
              "description": "logo: in the middle; halftone: a photo behind the code, drawn as dots"
            },
            "data": {
              "type": "string",
              "description": "data:image/png;base64,... (png, jpeg, webp or gif, up to 2 MB)"
            },
            "imageId": {
              "type": "string",
              "description": "The stored picture; send it back to keep it"
            },
            "size": {
              "type": "number",
              "minimum": 0.1,
              "maximum": 0.3,
              "description": "Logo side as a share of the code (default 0.22)"
            }
          }
        }
      }
    },
    "RedirectRule": {
      "type": "object",
      "required": [
        "when",
        "targetUrl"
      ],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,32}$",
          "description": "Generated if omitted; keep it when editing (visit stats are keyed by it)"
        },
        "name": {
          "type": "string",
          "maxLength": 100
        },
        "enabled": {
          "type": "boolean",
          "default": true
        },
        "when": {
          "$ref": "#/$defs/RuleCondition"
        },
        "targetUrl": {
          "type": "string",
          "format": "uri",
          "description": "http(s) only"
        }
      }
    },
    "RuleCondition": {
      "description": "A test, or all/any/not of conditions (at most 4 deep, 20 tests per rule). See /llms.txt for the field/op table.",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "field",
            "op"
          ],
          "additionalProperties": false,
          "properties": {
            "field": {
              "type": "string",
              "enum": [
                "os",
                "device",
                "browser",
                "language",
                "country",
                "referrer",
                "source",
                "query",
                "time",
                "weekday",
                "hour"
              ]
            },
            "op": {
              "type": "string",
              "enum": [
                "eq",
                "neq",
                "in",
                "not_in",
                "contains",
                "starts_with",
                "ends_with",
                "exists",
                "before",
                "after",
                "gte",
                "lte"
              ]
            },
            "value": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "number"
                },
                {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "number"
                      }
                    ]
                  }
                }
              ],
              "description": "Array for in/not_in; omitted for exists"
            },
            "key": {
              "type": "string",
              "description": "Query parameter name; only for field \"query\""
            }
          }
        },
        {
          "type": "object",
          "required": [
            "all"
          ],
          "additionalProperties": false,
          "properties": {
            "all": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/RuleCondition"
              }
            }
          }
        },
        {
          "type": "object",
          "required": [
            "any"
          ],
          "additionalProperties": false,
          "properties": {
            "any": {
              "type": "array",
              "items": {
                "$ref": "#/$defs/RuleCondition"
              }
            }
          }
        },
        {
          "type": "object",
          "required": [
            "not"
          ],
          "additionalProperties": false,
          "properties": {
            "not": {
              "$ref": "#/$defs/RuleCondition"
            }
          }
        }
      ]
    },
    "Slug": {
      "type": "string",
      "pattern": "^[a-zA-Z0-9_-]{3,64}$",
      "description": "Globally unique and immutable. Route names (health, v1, mcp, r, api, admin, www, ...) are reserved."
    },
    "Tags": {
      "type": "array",
      "maxItems": 20,
      "items": {
        "type": "string",
        "minLength": 1,
        "maxLength": 64
      }
    }
  }
}