{
  "openapi": "3.1.0",
  "info": {
    "title": "Arvore Link Shortener API",
    "version": "1.0.0"
  },
  "tags": [
    {
      "name": "public-api",
      "description": "Supported for third-party use, documented at /docs"
    }
  ],
  "servers": [
    {
      "url": "https://api.arvore.link",
      "description": "Production"
    },
    {
      "url": "https://api.{stage}.arvore.link",
      "description": "Development",
      "variables": {
        "stage": {
          "default": "rafael"
        }
      }
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API token, created at /links/conta and prefixed `arv_`. It does not\nexpire; revoke it when you are done with it. This is the scheme to use\nfrom anything that is not a browser.\n\nA token can do what its owner can to links, árvores and analytics, but\nnot administer the account: changing the email, deleting the account,\nrevoking sessions and creating further tokens all answer 403\nSESSION_REQUIRED and need a real sign-in. That way revoking a token\nactually ends its access.\n\nOn the free plan an account can have **one live token at a time** and\nmake **100 requests per hour**, counted per token. Revoking frees the\nslot, so rotating is revoke-then-create.\n"
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "arvore_session",
        "description": "How the arvore.link app itself authenticates — an httpOnly cookie set by\nPOST /auth/verify. Documented for completeness; a browser sends it\nautomatically and nothing else can.\n"
      }
    },
    "responses": {
      "SessionRequired": {
        "description": "SESSION_REQUIRED — this endpoint administers the account, so an API token is refused and a signed-in session is required. Keeping token creation and account changes behind a real sign-in is what makes revoking a token actually end its access.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "RATE_LIMITED — too many requests in the current window. The Retry-After header carries the seconds until it resets.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the window resets"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "parameters": {
      "From": {
        "name": "from",
        "in": "query",
        "required": false,
        "description": "Start of time range (Unix timestamp in milliseconds, inclusive)",
        "schema": {
          "type": "string",
          "example": "1771849500000"
        }
      },
      "To": {
        "name": "to",
        "in": "query",
        "required": false,
        "description": "End of time range (Unix timestamp in milliseconds, inclusive)",
        "schema": {
          "type": "string",
          "example": "1771849600000"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "`error` is always a string — a stable code, never an object. Validation failures add `fields` with the per-input detail, and set `error` to the specific code when the schema has one (`INVALID_TAG`, `INVALID_LINK_ID`) or `VALIDATION_FAILED` when it does not.\n",
        "properties": {
          "error": {
            "type": "string",
            "example": "VALIDATION_FAILED"
          },
          "fields": {
            "type": "object",
            "description": "Present on 400s. Maps each input to what was wrong with it.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "example": {
              "url": [
                "Invalid URL"
              ]
            }
          }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "tags": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string",
              "pattern": "^[0-9a-z-]{1,24}$"
            },
            "description": "For grouping links so they can be found later. Lowercase, digits and hyphens, at most 10 per link. Always present on read, `[]` for an untagged link.\n",
            "example": [
              "campanha-verao",
              "instagram"
            ]
          },
          "expires_at": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "description": "When the link stops working, in milliseconds since the epoch. Always present on read, `null` when the link does not expire. After this moment the link redirects to `expiration_url` if one is set, and answers 404 otherwise.\n",
            "example": 1785400000000
          },
          "expiration_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Where an expired link goes instead of answering 404. Always present on read, `null` when unset. Only meaningful alongside `expires_at`.\n",
            "example": "https://example.com/campaign-over"
          },
          "targeting": {
            "type": "object",
            "nullable": true,
            "description": "Per-visitor destinations. Always present on read, `null` when the link has none. Evaluated on every request: the operating system rule wins over the country rule, and a visitor matching neither goes to `url`. An expired link ignores targeting entirely.\n",
            "properties": {
              "geo": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "format": "uri"
                },
                "description": "Country code (ISO 3166-1 alpha-2, uppercase, as CloudFront reports it) to destination. At most 50 entries.\n",
                "example": {
                  "BR": "https://example.com.br",
                  "PT": "https://example.pt"
                }
              },
              "ios": {
                "type": "string",
                "format": "uri",
                "description": "Where iOS and iPadOS visitors go. Both, deliberately — an iPad user sent to the Play Store is the bug this feature exists to prevent.\n",
                "example": "https://apps.apple.com/app/id1234567890"
              },
              "android": {
                "type": "string",
                "format": "uri",
                "example": "https://play.google.com/store/apps/details?id=com.example"
              }
            }
          },
          "has_password": {
            "type": "boolean",
            "description": "Whether a password is required to open the link. A boolean and never the hash: the hash is stored only on the record the redirect app reads, so there is nothing here to leak. The password itself is write-only and can never be read back — the same once-only property an API token has.\n",
            "example": false
          },
          "og_title": {
            "type": "string",
            "nullable": true,
            "maxLength": 120,
            "description": "Custom title for the share card. When any og_ field is set, the redirect answers HTML carrying these tags to social scrapers instead of a 301; everyone else, including search crawlers, still gets the 301.\n",
            "example": "Promotion ends Sunday"
          },
          "og_description": {
            "type": "string",
            "nullable": true,
            "maxLength": 240,
            "example": "Free shipping on everything"
          },
          "og_image": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Image for the share card, 1200x630. Any absolute http(s) URL — it does not have to be hosted here. When unset but a title is present, an image is generated from the title, so a card needs no upload to exist.\n",
            "example": "https://assets.arvore.link/avatars/ws000001/og-a1b2c3d4-x9.webp"
          },
          "id": {
            "type": "string",
            "description": "Internal link id, 8 characters",
            "example": "a1b2c3d4"
          },
          "slug": {
            "type": "string",
            "description": "Path on the redirect domain",
            "example": "a1b2"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://example.com"
          },
          "short_url": {
            "type": "string",
            "format": "uri",
            "example": "https://arvr.ink/a1b2"
          },
          "redirect_domain": {
            "type": "string",
            "example": "arvr.ink"
          },
          "created_at": {
            "type": "integer",
            "description": "Unix timestamp in milliseconds",
            "example": 1739111400000
          },
          "updated_at": {
            "type": "integer",
            "description": "Unix timestamp in milliseconds",
            "example": 1739111400000
          },
          "deleted_at": {
            "type": "integer",
            "nullable": true,
            "example": null
          }
        }
      },
      "Linktree": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "k9fx2n1a"
          },
          "slug": {
            "type": "string",
            "description": "Path on the redirect domain, 1-50 chars of [0-9a-z-]",
            "example": "meu-perfil"
          },
          "title": {
            "type": "string",
            "example": "minha página"
          },
          "bio": {
            "type": "string",
            "nullable": true,
            "example": "links que importam"
          },
          "avatar_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "example": "https://example.com/foto.jpg"
          },
          "published": {
            "type": "boolean",
            "description": "Unpublished árvores 404 on the public page",
            "example": false
          },
          "created_at": {
            "type": "integer",
            "example": 1739111400000
          },
          "updated_at": {
            "type": "integer",
            "example": 1739111400000
          }
        }
      },
      "LinktreeItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "p3qr7s2t"
          },
          "title": {
            "type": "string",
            "example": "instagram"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Final destination",
            "example": "https://instagram.com/perfil"
          },
          "short_url": {
            "type": "string",
            "format": "uri",
            "description": "Short link the public page points at, so clicks are counted",
            "example": "https://arvr.ink/x7y8"
          },
          "position": {
            "type": "integer",
            "description": "Ascending order on the public page",
            "example": 0
          },
          "active": {
            "type": "boolean",
            "description": "Inactive items are hidden from the public page",
            "example": true
          },
          "created_at": {
            "type": "integer",
            "example": 1739111400000
          },
          "updated_at": {
            "type": "integer",
            "example": 1739111400000
          }
        }
      },
      "LinktreeAnalytics": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "k9fx2n1a"
          },
          "slug": {
            "type": "string",
            "example": "meu-perfil"
          },
          "title": {
            "type": "string",
            "example": "minha página"
          },
          "published": {
            "type": "boolean",
            "example": true
          },
          "views": {
            "type": "integer",
            "description": "Visits to the public page in the range",
            "example": 128
          },
          "clicks": {
            "type": "integer",
            "description": "Sum of item clicks in the range",
            "example": 41
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "example": "p3qr7s2t"
                },
                "title": {
                  "type": "string",
                  "example": "instagram"
                },
                "short_url": {
                  "type": "string",
                  "format": "uri",
                  "nullable": true,
                  "example": "https://arvr.ink/x7y8"
                },
                "active": {
                  "type": "boolean",
                  "example": true
                },
                "count": {
                  "type": "integer",
                  "example": 22
                }
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "example": "a1b2c3d4"
              },
              "email": {
                "type": "string",
                "format": "email",
                "example": "voce@exemplo.com"
              },
              "name": {
                "type": "string",
                "nullable": true,
                "example": "Rafael"
              },
              "avatar_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "example": "https://assets.arvore.link/avatars/w1/user-a1b2c3d4-k9fx2n1a.webp"
              },
              "created_at": {
                "type": "integer",
                "example": 1739111400000
              }
            }
          },
          "workspace": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "example": "w1x2y3z4"
              },
              "name": {
                "type": "string",
                "nullable": true,
                "example": "Pessoal"
              }
            }
          }
        }
      },
      "ApiToken": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifies the token for revocation — not the token itself",
            "example": "tk8f2c1a"
          },
          "name": {
            "type": "string",
            "example": "deploy da CI"
          },
          "prefix": {
            "type": "string",
            "description": "The first characters of the real token, to recognise it by",
            "example": "arv_9f2c…"
          },
          "created_at": {
            "type": "integer",
            "example": 1785000000000
          },
          "last_used_at": {
            "type": "integer",
            "nullable": true,
            "description": "Updated at most once every five minutes, so it is accurate to within that, not to the request. Null until first used.\n",
            "example": 1785003600000
          },
          "revoked_at": {
            "type": "integer",
            "nullable": true,
            "description": "When it was revoked, or null while it still works. A revoked token keeps its row so the list can show that it existed and when it stopped being trusted.\n",
            "example": 1785007200000
          }
        }
      },
      "Session": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Last six characters of the token — never the token itself",
            "example": "7s2t9x"
          },
          "created_at": {
            "type": "integer",
            "example": 1739111400000
          },
          "expires_at": {
            "type": "integer",
            "example": 1741703400000
          },
          "current": {
            "type": "boolean",
            "description": "True for the session making the request",
            "example": true
          },
          "device": {
            "type": "string",
            "nullable": true,
            "enum": [
              "desktop",
              "mobile",
              "tablet",
              "smarttv",
              null
            ],
            "description": "From the CloudFront device hints, falling back to the User-Agent",
            "example": "mobile"
          },
          "browser": {
            "type": "string",
            "nullable": true,
            "example": "Chrome 145"
          },
          "platform": {
            "type": "string",
            "nullable": true,
            "example": "iOS"
          },
          "country": {
            "type": "string",
            "nullable": true,
            "description": "From the CloudFront viewer headers, when present",
            "example": "BR"
          },
          "region_name": {
            "type": "string",
            "nullable": true,
            "example": "São Paulo"
          },
          "city": {
            "type": "string",
            "nullable": true,
            "example": "São Paulo"
          }
        }
      },
      "ClickEvent": {
        "type": "object",
        "properties": {
          "ts": {
            "type": "string",
            "description": "Sort key in the format \"{timestamp_ms}#{random}\"",
            "example": "1771849585571#k9fx2n1a"
          },
          "timestamp": {
            "type": "integer",
            "description": "Unix timestamp in milliseconds",
            "example": 1771849585571
          },
          "country": {
            "type": "string",
            "nullable": true,
            "example": "BR"
          },
          "region": {
            "type": "string",
            "nullable": true,
            "example": "SP"
          },
          "region_name": {
            "type": "string",
            "nullable": true,
            "example": "São Paulo"
          },
          "timezone": {
            "type": "string",
            "nullable": true,
            "example": "America/Sao_Paulo"
          },
          "lat": {
            "type": "number",
            "nullable": true,
            "example": -23.673
          },
          "lng": {
            "type": "number",
            "nullable": true,
            "example": -46.5302
          },
          "device": {
            "type": "string",
            "enum": [
              "mobile",
              "tablet",
              "smarttv",
              "desktop"
            ],
            "example": "desktop"
          },
          "platform": {
            "type": "string",
            "nullable": true,
            "example": "macOS"
          },
          "browser": {
            "type": "string",
            "nullable": true,
            "example": "Chrome 145"
          },
          "referer": {
            "type": "string",
            "nullable": true,
            "example": "https://twitter.com"
          }
        }
      }
    }
  },
  "paths": {
    "/account": {
      "get": {
        "summary": "Get the signed-in user and workspace",
        "tags": [
          "public-api"
        ],
        "x-pt": "descobrir de quem é o token e qual workspace ele acessa",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/reports": {
      "post": {
        "summary": "Report a short link for abuse",
        "tags": [
          "public-api"
        ],
        "x-pt": "denunciar um link curto",
        "description": "Flags a short link for a human to review. **A report never changes what the link\ndoes** — anything automatic would hand everyone a button for taking down each\nother's links, and abusing the reporting system would be easier than the abuse\nit prevents. An admin decides.\n\nPublic and unauthenticated, because somebody who received a phishing link has no\naccount here and never will. Capped twice: per caller address, and per reported\nslug — the second stops one link being reported enough times to bury every other\nreport, which is the denial of service a reporting form actually invites.\n\n`url` must be a link on the redirect domain; anything else answers 400\nNOT_A_SHORT_LINK rather than being stored, or the queue becomes a dumping ground\nan admin has to wade through. A report about a slug that does not exist answers\n200 and stores nothing — a 404 would make this a slug oracle.\n\n**Part of the public API**, unlike the other unauthenticated writes here. The\nfirst-party beacons are excluded because a stranger writing into our analytics\nis not a feature; for this endpoint a stranger writing *is* the feature. An\nabuse desk, a brand-protection service or a threat feed reporting in bulk is\nexactly who it exists for, and making them reverse-engineer it would be\nperverse.\n\nNo authentication and none accepted — a token buys nothing here. Reports are\njudged on their content by a person, so an identified reporter gets no more\nweight than an anonymous one, and the rate limits apply either way.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "reason"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "The short link, as pasted. A bare slug is accepted too.",
                    "example": "https://arvr.ink/a1b2"
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "phishing",
                      "malware",
                      "spam",
                      "fraude",
                      "outro"
                    ],
                    "example": "phishing"
                  },
                  "detail": {
                    "type": "string",
                    "maxLength": 1000,
                    "example": "Recebi por SMS dizendo que era o banco"
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 200,
                    "description": "Optional and not verified. Requiring an address would filter out the person this exists for; it is only so an admin can follow up.\n",
                    "example": "quem@reportou.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report received, or silently discarded for an unknown slug",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "NOT_A_SHORT_LINK or a validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/links": {
      "get": {
        "summary": "List short links",
        "tags": [
          "public-api"
        ],
        "x-pt": "listar seus links curtos",
        "description": "Links in the caller's workspace, newest first. Deleted links and\nlinktree-owned links are excluded — an árvore and each of its items own\na link record, and those are reported through /linktrees.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Links",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "links": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Link"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create short link",
        "tags": [
          "public-api"
        ],
        "x-pt": "criar um link curto",
        "description": "Creates a short link for the given URL. Authentication is optional —\nauthenticated links are scoped to the user's workspace, unauthenticated\nlinks are stored under \"public\". A slug is generated when omitted.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://example.com"
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[0-9a-z-]{1,50}$",
                    "example": "meu-link"
                  },
                  "tags": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "pattern": "^[0-9a-z-]{1,24}$"
                    },
                    "description": "Lowercase, digits and hyphens, at most 10 per link. Deduplicated and sorted on write. Send `[]` to clear them; omit the field to leave them untouched.\n",
                    "example": [
                      "campanha-verao",
                      "instagram"
                    ]
                  },
                  "expires_at": {
                    "type": "integer",
                    "format": "int64",
                    "description": "When the link stops working, in milliseconds since the epoch. Must be in the future — a past timestamp answers 400 EXPIRES_AT_IN_THE_PAST, since it is almost always a timezone mistake and accepting it would create a link that is already dead.\n",
                    "example": 1785400000000
                  },
                  "expiration_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where an expired link goes instead of answering 404. Requires `expires_at` — on its own it answers 400 EXPIRATION_URL_WITHOUT_EXPIRY rather than being stored and never used.\n",
                    "example": "https://example.com/campaign-over"
                  },
                  "targeting": {
                    "type": "object",
                    "nullable": true,
                    "description": "Per-visitor destinations. Replaced wholesale rather than merged — send the full set of rules, or `null` to remove them all. Merging would make removing one country impossible, since there would be no way to express its absence.\n",
                    "properties": {
                      "geo": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string",
                          "format": "uri"
                        },
                        "description": "Country code (ISO 3166-1 alpha-2) to destination. Upper-cased on write, since CloudFront reports uppercase and a lowercase key would be a rule that can never match. At most 50 entries (TOO_MANY_RULES).\n",
                        "example": {
                          "BR": "https://example.com.br"
                        }
                      },
                      "ios": {
                        "type": "string",
                        "format": "uri",
                        "example": "https://apps.apple.com/app/id1234567890"
                      },
                      "android": {
                        "type": "string",
                        "format": "uri",
                        "example": "https://play.google.com/store/apps/details?id=com.example"
                      }
                    }
                  },
                  "password": {
                    "type": "string",
                    "minLength": 4,
                    "maxLength": 200,
                    "writeOnly": true,
                    "description": "A password visitors must type before being redirected. Hashed with salted scrypt on write; never returned by any endpoint, so it cannot be read back once set. Send `null` to remove it (PATCH only); omit to leave it alone. Note this gates the destination rather than hiding it: anyone who learns the password can pass it on.\n",
                    "example": "verao2026"
                  },
                  "og_title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 120,
                    "description": "Custom title for the share card. Setting any og_ field makes the link serve HTML to social scrapers rather than a 301.\n",
                    "example": "Promotion ends Sunday"
                  },
                  "og_description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 240,
                    "example": "Free shipping on everything"
                  },
                  "og_image": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true,
                    "description": "Any absolute http(s) URL, 1200x630 recommended. Point at an image you already host — there is no binary upload in the public API, for the same reason avatar uploads are not part of it. Omit it and an image is generated from the title.\n",
                    "example": "https://example.com/card.png"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Link created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "LINK_ID_ALREADY_IN_USE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/links/{id}": {
      "patch": {
        "summary": "Update a short link",
        "tags": [
          "public-api"
        ],
        "x-pt": "editar destino, id, tags, expiração, segmentação, senha ou prévia",
        "description": "Changes the destination URL, the slug, the tags or the expiry. Every field\nfollows the same three cases: omit it to leave the stored value alone, send\n`null` (or `[]` for tags) to clear it, send a value to set it. Linktree-owned\nlinks are rejected: writing through here would rebuild the `slug:` record as\na plain short link and make the árvore unreachable.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "a1b2c3d4"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[0-9a-z-]{1,50}$"
                  },
                  "tags": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "pattern": "^[0-9a-z-]{1,24}$"
                    },
                    "description": "Lowercase, digits and hyphens, at most 10 per link. Deduplicated and sorted on write. Send `[]` to clear them; omit the field to leave them untouched.\n",
                    "example": [
                      "campanha-verao",
                      "instagram"
                    ]
                  },
                  "expires_at": {
                    "type": "integer",
                    "format": "int64",
                    "nullable": true,
                    "description": "When the link stops working, in milliseconds since the epoch. Must be in the future. Send `null` to remove an expiry; omit the field to leave it untouched.\n",
                    "example": 1785400000000
                  },
                  "expiration_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true,
                    "description": "Where an expired link goes instead of answering 404. Requires an expiry, whether sent in the same request or already stored — otherwise 400 EXPIRATION_URL_WITHOUT_EXPIRY. Send `null` to remove it.\n",
                    "example": "https://example.com/campaign-over"
                  },
                  "targeting": {
                    "type": "object",
                    "nullable": true,
                    "description": "Per-visitor destinations. Replaced wholesale rather than merged — send the full set of rules, or `null` to remove them all. Merging would make removing one country impossible, since there would be no way to express its absence.\n",
                    "properties": {
                      "geo": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string",
                          "format": "uri"
                        },
                        "description": "Country code (ISO 3166-1 alpha-2) to destination. Upper-cased on write, since CloudFront reports uppercase and a lowercase key would be a rule that can never match. At most 50 entries (TOO_MANY_RULES).\n",
                        "example": {
                          "BR": "https://example.com.br"
                        }
                      },
                      "ios": {
                        "type": "string",
                        "format": "uri",
                        "example": "https://apps.apple.com/app/id1234567890"
                      },
                      "android": {
                        "type": "string",
                        "format": "uri",
                        "example": "https://play.google.com/store/apps/details?id=com.example"
                      }
                    }
                  },
                  "password": {
                    "type": "string",
                    "minLength": 4,
                    "maxLength": 200,
                    "writeOnly": true,
                    "description": "A password visitors must type before being redirected. Hashed with salted scrypt on write; never returned by any endpoint, so it cannot be read back once set. Send `null` to remove it (PATCH only); omit to leave it alone. Note this gates the destination rather than hiding it: anyone who learns the password can pass it on.\n",
                    "example": "verao2026"
                  },
                  "og_title": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 120,
                    "description": "Custom title for the share card. Setting any og_ field makes the link serve HTML to social scrapers rather than a 301.\n",
                    "example": "Promotion ends Sunday"
                  },
                  "og_description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 240,
                    "example": "Free shipping on everything"
                  },
                  "og_image": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true,
                    "description": "Any absolute http(s) URL, 1200x630 recommended. Point at an image you already host — there is no binary upload in the public API, for the same reason avatar uploads are not part of it. Omit it and an image is generated from the title.\n",
                    "example": "https://example.com/card.png"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Link not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "LINK_ID_ALREADY_IN_USE or LINKTREE_LINK_NOT_EDITABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a short link",
        "tags": [
          "public-api"
        ],
        "x-pt": "apagar um link curto",
        "description": "Frees the slug and stops the link redirecting on the next request. The link\nrecord itself is kept with a `deleted_at` timestamp and disappears from\n`GET /links`.\n\nThat split is deliberate rather than a compromise. Freeing the name is the\noutcome anyone deleting a link wants, and the 4-character namespace is shared\nand finite. Keeping the row is what an abuse takedown needs — the link stops\nworking while the record survives as evidence of what it pointed at. Click\nevents are kept for the same reason; they are keyed by link id, so a future\nlink reusing the slug never collides with them.\n\nAnswers 204 whether or not the link was already deleted, so a retry after a\ntimeout is safe. Deleting the account removes everything for real.\n\nÁrvore-owned links are refused: removing one would take the `slug:` record\nwith it and make the árvore page or one of its buttons unreachable. Use\n`DELETE /linktrees/{id}` or the item endpoint instead.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "a1b2c3d4"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted, or already was"
          },
          "404": {
            "description": "NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "LINKTREE_LINK_NOT_EDITABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/links/{id}/analytics": {
      "get": {
        "summary": "Get click analytics for a link",
        "tags": [
          "public-api"
        ],
        "x-pt": "ver os cliques de um link",
        "description": "Click events for one short link, newest first, capped at 500.",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "a1b2c3d4"
            }
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "example": 42
                    },
                    "clicks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClickEvent"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Link not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/analytics": {
      "get": {
        "summary": "Get click analytics across links",
        "tags": [
          "public-api"
        ],
        "x-pt": "ver os cliques de todos os seus links",
        "description": "Click events for every short link in the workspace, or one link when\nlink_id is given, capped at 500 per link. Linktree-owned links are\nexcluded, matching GET /links.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "link_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "a1b2c3d4"
            }
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "example": 128
                    },
                    "clicks": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/ClickEvent"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "link_id": {
                                "type": "string",
                                "example": "a1b2c3d4"
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Link not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/linktrees": {
      "get": {
        "summary": "List árvores",
        "tags": [
          "public-api"
        ],
        "x-pt": "listar suas árvores",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Linktrees",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "linktrees": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Linktree"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an árvore",
        "tags": [
          "public-api"
        ],
        "x-pt": "criar uma árvore",
        "description": "Reserves the slug and creates the profile, unpublished. A link record is\ncreated alongside it so page views can be counted.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug",
                  "title"
                ],
                "properties": {
                  "slug": {
                    "type": "string",
                    "pattern": "^[0-9a-z-]{1,50}$",
                    "example": "meu-perfil"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "bio": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "avatar_url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Linktree created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Linktree"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "SLUG_ALREADY_IN_USE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/linktrees/{id}": {
      "get": {
        "summary": "Get an árvore with its items",
        "tags": [
          "public-api"
        ],
        "x-pt": "ver uma árvore com seus itens",
        "description": "Includes inactive items, ordered by position.",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Linktree with items",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Linktree"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/LinktreeItem"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an árvore",
        "tags": [
          "public-api"
        ],
        "x-pt": "editar o perfil, o slug ou publicar uma árvore",
        "description": "Renaming the slug moves the reservation and rewrites the short_url on the\nárvore's link record.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "slug": {
                    "type": "string",
                    "pattern": "^[0-9a-z-]{1,50}$"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "bio": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 300
                  },
                  "avatar_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true
                  },
                  "published": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Linktree updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Linktree"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "SLUG_ALREADY_IN_USE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an árvore",
        "tags": [
          "public-api"
        ],
        "x-pt": "apagar uma árvore",
        "description": "Removes the profile, its items, the slug reservation and the link records\ncreated for the page and its items. Click events are left in place.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/linktrees/{id}/analytics": {
      "get": {
        "summary": "Get analytics for one árvore",
        "tags": [
          "public-api"
        ],
        "x-pt": "ver as visitas e os cliques de uma árvore",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinktreeAnalytics"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/linktrees/{id}/items": {
      "post": {
        "summary": "Add an item to an árvore",
        "tags": [
          "public-api"
        ],
        "x-pt": "adicionar um item na árvore",
        "description": "Creates a short link for the destination URL and points the item at it,\nso clicks are counted. Position defaults to the end of the list.\n",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "url"
                ],
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 100,
                    "example": "instagram"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://instagram.com/perfil"
                  },
                  "position": {
                    "type": "integer",
                    "minimum": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinktreeItem"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Linktree not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/linktrees/{id}/items/{itemId}": {
      "patch": {
        "summary": "Update an item",
        "tags": [
          "public-api"
        ],
        "x-pt": "editar um item da árvore",
        "description": "Changing the URL repoints the item's short link at the new destination.",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          },
          {
            "name": "itemId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "p3qr7s2t"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "position": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinktreeItem"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Linktree or item not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an item",
        "tags": [
          "public-api"
        ],
        "x-pt": "apagar um item da árvore",
        "description": "Removes the item along with the short link created for it.",
        "security": [
          {
            "bearer": []
          },
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "k9fx2n1a"
            }
          },
          {
            "name": "itemId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "p3qr7s2t"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Linktree or item not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
