{
  "openapi": "3.1.0",
  "info": {
    "title": "Bio API",
    "version": "1.1.0",
    "summary": "Free link in bio pages, a free alternative to Linktree and Beacons.",
    "description": "Make a link in bio page in one call: a photo, a name, a short bio, links and headers, social icons and a QR code at https://bio-free.openapps.xyz/<handle>. No account and no API key: making a page returns an ownerKey and an editUrl. The ownerKey (header x-owner-key) changes the page and reads its views and clicks; the editUrl opens the private editor on any device. Both are passwords for that page. Errors are JSON with an error sentence that says what to change. Free with no limits. Full guide: https://bio-free.openapps.xyz/llms.txt",
    "contact": {
      "name": "OpenApps",
      "url": "https://free-apps.openapps.xyz/"
    }
  },
  "servers": [
    {
      "url": "https://functions.bounded.sh/apps/4ce232ee856d7de1ba5f2b6d/api"
    }
  ],
  "externalDocs": {
    "description": "llms.txt",
    "url": "https://bio-free.openapps.xyz/llms.txt"
  },
  "paths": {
    "/": {
      "get": {
        "operationId": "describe",
        "summary": "Describe the API and its routes",
        "responses": {
          "200": {
            "description": "Name, docs and routes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "about": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    },
                    "openapi": {
                      "type": "string"
                    },
                    "routes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "name": "Bio by OpenApps",
                  "docs": "https://bio-free.openapps.xyz/llms.txt",
                  "openapi": "https://bio-free.openapps.xyz/openapi.json",
                  "routes": [
                    "POST /pages ...",
                    "GET /pages/:handle ..."
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/pages": {
      "post": {
        "operationId": "createPage",
        "summary": "Make a page",
        "description": "Returns the page with its private editUrl and ownerKey. Give the person the url for their profile and the editUrl to change it later.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "handle",
                  "name"
                ],
                "properties": {
                  "handle": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$",
                    "description": "The page address. \"@Maya\" becomes \"maya\". Some words are reserved."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "bio": {
                    "type": "string",
                    "maxLength": 160,
                    "description": "One line."
                  },
                  "avatar": {
                    "type": "string",
                    "maxLength": 60000,
                    "description": "PNG, JPEG or WebP data URL. Most callers leave it out; the owner can upload a photo in the editor."
                  },
                  "links": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/LinkInput"
                    },
                    "description": "Replaces the whole list. Send hidden items back too."
                  },
                  "socials": {
                    "$ref": "#/components/schemas/SocialsInput"
                  },
                  "theme": {
                    "type": "string",
                    "enum": [
                      "paper",
                      "ink",
                      "cobalt",
                      "sage",
                      "night"
                    ]
                  },
                  "buttonStyle": {
                    "type": "string",
                    "enum": [
                      "filled",
                      "outline"
                    ]
                  },
                  "showCredit": {
                    "type": "boolean",
                    "description": "Shows \"Made with Bio\" at the bottom."
                  }
                }
              },
              "example": {
                "handle": "maya-ceramics",
                "name": "Maya Okafor",
                "bio": "Ceramics and slow mornings in Lisbon.",
                "links": [
                  {
                    "title": "Shop",
                    "header": true
                  },
                  {
                    "title": "Shop the October pieces",
                    "url": "https://example.com/shop"
                  },
                  {
                    "title": "Wheel classes",
                    "url": "example.com/classes"
                  }
                ],
                "socials": {
                  "instagram": "@maya",
                  "email": "maya@example.com"
                },
                "theme": "paper"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new page, with its private edit link and owner key",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "editUrl": {
                          "type": "string"
                        },
                        "ownerKey": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "handle": "maya-ceramics",
                  "url": "https://bio-free.openapps.xyz/maya-ceramics",
                  "name": "Maya Okafor",
                  "bio": "Ceramics and slow mornings in Lisbon.",
                  "avatar": "",
                  "theme": "paper",
                  "buttonStyle": "filled",
                  "links": [
                    {
                      "id": "Xy12ab34",
                      "title": "Shop",
                      "url": "",
                      "header": true
                    },
                    {
                      "id": "Pq98rs76",
                      "title": "Shop the October pieces",
                      "url": "https://example.com/shop"
                    },
                    {
                      "id": "Lm45no12",
                      "title": "Summer sale",
                      "url": "https://example.com/sale",
                      "hidden": true
                    }
                  ],
                  "socials": {
                    "email": "maya@example.com",
                    "instagram": "https://www.instagram.com/maya"
                  },
                  "showCredit": true,
                  "views": 0,
                  "createdAt": "2026-10-19T12:00:00.000Z",
                  "editUrl": "https://bio-free.openapps.xyz/e/maya-ceramics#k=OWNER_KEY",
                  "ownerKey": "OWNER_KEY"
                }
              }
            }
          },
          "400": {
            "description": "Something in the body needs changing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Link 2: That does not look like a web address."
                }
              }
            }
          },
          "409": {
            "description": "The handle is taken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "bio-free.openapps.xyz/maya-ceramics is taken. Try another handle."
                }
              }
            }
          }
        }
      }
    },
    "/pages/{handle}": {
      "get": {
        "operationId": "getPage",
        "summary": "Read a page (counts nothing)",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$"
            },
            "example": "maya-ceramics"
          }
        ],
        "responses": {
          "200": {
            "description": "The page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                },
                "example": {
                  "handle": "maya-ceramics",
                  "url": "https://bio-free.openapps.xyz/maya-ceramics",
                  "name": "Maya Okafor",
                  "bio": "Ceramics and slow mornings in Lisbon.",
                  "avatar": "",
                  "theme": "paper",
                  "buttonStyle": "filled",
                  "links": [
                    {
                      "id": "Xy12ab34",
                      "title": "Shop",
                      "url": "",
                      "header": true
                    },
                    {
                      "id": "Pq98rs76",
                      "title": "Shop the October pieces",
                      "url": "https://example.com/shop"
                    },
                    {
                      "id": "Lm45no12",
                      "title": "Summer sale",
                      "url": "https://example.com/sale",
                      "hidden": true
                    }
                  ],
                  "socials": {
                    "email": "maya@example.com",
                    "instagram": "https://www.instagram.com/maya"
                  },
                  "showCredit": true,
                  "views": 120,
                  "createdAt": "2026-10-19T12:00:00.000Z"
                }
              }
            }
          },
          "404": {
            "description": "No page at this handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "There is no page at this address."
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updatePage",
        "summary": "Change a page (owner)",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$"
            },
            "example": "maya-ceramics"
          },
          {
            "name": "x-owner-key",
            "in": "header",
            "required": true,
            "description": "The ownerKey returned when the page was made. Treat it like a password.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "description": "Send any fields to change. links and socials replace the whole list: read the page first and send every item back, keeping each link's id so its click count stays with it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "bio": {
                    "type": "string",
                    "maxLength": 160,
                    "description": "One line."
                  },
                  "avatar": {
                    "type": "string",
                    "maxLength": 60000,
                    "description": "PNG, JPEG or WebP data URL. Most callers leave it out; the owner can upload a photo in the editor."
                  },
                  "links": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/LinkInput"
                    },
                    "description": "Replaces the whole list. Send hidden items back too."
                  },
                  "socials": {
                    "$ref": "#/components/schemas/SocialsInput"
                  },
                  "theme": {
                    "type": "string",
                    "enum": [
                      "paper",
                      "ink",
                      "cobalt",
                      "sage",
                      "night"
                    ]
                  },
                  "buttonStyle": {
                    "type": "string",
                    "enum": [
                      "filled",
                      "outline"
                    ]
                  },
                  "showCredit": {
                    "type": "boolean",
                    "description": "Shows \"Made with Bio\" at the bottom."
                  }
                }
              },
              "example": {
                "bio": "New pieces every first Sunday.",
                "theme": "sage"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                },
                "example": {
                  "handle": "maya-ceramics",
                  "url": "https://bio-free.openapps.xyz/maya-ceramics",
                  "name": "Maya Okafor",
                  "bio": "Ceramics and slow mornings in Lisbon.",
                  "avatar": "",
                  "theme": "sage",
                  "buttonStyle": "filled",
                  "links": [
                    {
                      "id": "Xy12ab34",
                      "title": "Shop",
                      "url": "",
                      "header": true
                    },
                    {
                      "id": "Pq98rs76",
                      "title": "Shop the October pieces",
                      "url": "https://example.com/shop"
                    },
                    {
                      "id": "Lm45no12",
                      "title": "Summer sale",
                      "url": "https://example.com/sale",
                      "hidden": true
                    }
                  ],
                  "socials": {
                    "email": "maya@example.com",
                    "instagram": "https://www.instagram.com/maya"
                  },
                  "showCredit": true,
                  "views": 120,
                  "createdAt": "2026-10-19T12:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Something in the body needs changing",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "theme must be one of paper, ink, cobalt, sage, night."
                }
              }
            }
          },
          "401": {
            "description": "No owner key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Send the page's owner key in the x-owner-key header."
                }
              }
            }
          },
          "403": {
            "description": "Wrong owner key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "That owner key does not match this page."
                }
              }
            }
          },
          "404": {
            "description": "No page at this handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "There is no page at this address."
                }
              }
            }
          }
        }
      }
    },
    "/pages/{handle}/stats": {
      "get": {
        "operationId": "getStats",
        "summary": "Views and clicks per link (owner)",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$"
            },
            "example": "maya-ceramics"
          },
          {
            "name": "x-owner-key",
            "in": "header",
            "required": true,
            "description": "The ownerKey returned when the page was made. Treat it like a password.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totals and clicks per link (headers left out)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Stats"
                },
                "example": {
                  "handle": "maya-ceramics",
                  "url": "https://bio-free.openapps.xyz/maya-ceramics",
                  "views": 120,
                  "clicks": 48,
                  "links": [
                    {
                      "id": "Pq98rs76",
                      "title": "Shop the October pieces",
                      "url": "https://example.com/shop",
                      "clicks": 48
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No owner key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Send the page's owner key in the x-owner-key header."
                }
              }
            }
          },
          "403": {
            "description": "Wrong owner key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "That owner key does not match this page."
                }
              }
            }
          },
          "404": {
            "description": "No page at this handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "There is no page at this address."
                }
              }
            }
          }
        }
      }
    },
    "/pages/{handle}/qr.svg": {
      "get": {
        "operationId": "getQrCode",
        "summary": "The page's QR code as an SVG image",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$"
            },
            "example": "maya-ceramics"
          }
        ],
        "responses": {
          "200": {
            "description": "Black on white SVG of the page address",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No page at this handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "There is no page at this address."
                }
              }
            }
          }
        }
      }
    },
    "/view/{handle}": {
      "get": {
        "operationId": "viewPage",
        "summary": "Read a page and count one view (what the page itself calls)",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$"
            },
            "example": "maya-ceramics"
          }
        ],
        "description": "Only for showing the page to a person. Use GET /pages/{handle} for anything else.",
        "responses": {
          "200": {
            "description": "The page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page"
                },
                "example": {
                  "handle": "maya-ceramics",
                  "url": "https://bio-free.openapps.xyz/maya-ceramics",
                  "name": "Maya Okafor",
                  "bio": "Ceramics and slow mornings in Lisbon.",
                  "avatar": "",
                  "theme": "paper",
                  "buttonStyle": "filled",
                  "links": [
                    {
                      "id": "Xy12ab34",
                      "title": "Shop",
                      "url": "",
                      "header": true
                    },
                    {
                      "id": "Pq98rs76",
                      "title": "Shop the October pieces",
                      "url": "https://example.com/shop"
                    },
                    {
                      "id": "Lm45no12",
                      "title": "Summer sale",
                      "url": "https://example.com/sale",
                      "hidden": true
                    }
                  ],
                  "socials": {
                    "email": "maya@example.com",
                    "instagram": "https://www.instagram.com/maya"
                  },
                  "showCredit": true,
                  "views": 120,
                  "createdAt": "2026-10-19T12:00:00.000Z"
                }
              }
            }
          },
          "404": {
            "description": "No page at this handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "There is no page at this address."
                }
              }
            }
          }
        }
      }
    },
    "/go/{handle}/{linkId}": {
      "get": {
        "operationId": "followLink",
        "summary": "Where a link goes, counting one click (what the page itself calls)",
        "parameters": [
          {
            "name": "handle",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$"
            },
            "example": "maya-ceramics"
          },
          {
            "name": "linkId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "Pq98rs76"
          }
        ],
        "responses": {
          "200": {
            "description": "The link's address",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "url": "https://example.com/shop"
                }
              }
            }
          },
          "404": {
            "description": "The link is not on the page (removed, hidden or a header)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This link is no longer on the page."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A sentence that says what to change."
          }
        }
      },
      "Link": {
        "type": "object",
        "required": [
          "id",
          "title",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Keep it when sending the link back, so its click count stays."
          },
          "title": {
            "type": "string",
            "maxLength": 80
          },
          "url": {
            "type": "string",
            "description": "Empty for a header."
          },
          "header": {
            "type": "boolean",
            "description": "True for a header: a title between links, with no url."
          },
          "hidden": {
            "type": "boolean",
            "description": "True when the item is kept but not shown on the page."
          }
        }
      },
      "LinkInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "maxLength": 80,
            "description": "Optional for a link (defaults to the address), required for a header."
          },
          "url": {
            "type": "string",
            "maxLength": 2000,
            "description": "http or https. \"example.com\" becomes \"https://example.com\". Not used for a header."
          },
          "header": {
            "type": "boolean"
          },
          "hidden": {
            "type": "boolean"
          }
        }
      },
      "SocialsInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "A full address on that site, or just the name. WhatsApp takes a phone number with its country code; Spotify needs the full open.spotify.com address.",
        "properties": {
          "website": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "x": {
            "type": "string"
          },
          "instagram": {
            "type": "string"
          },
          "tiktok": {
            "type": "string"
          },
          "youtube": {
            "type": "string"
          },
          "linkedin": {
            "type": "string"
          },
          "github": {
            "type": "string"
          },
          "threads": {
            "type": "string"
          },
          "bluesky": {
            "type": "string"
          },
          "facebook": {
            "type": "string"
          },
          "whatsapp": {
            "type": "string"
          },
          "telegram": {
            "type": "string"
          },
          "spotify": {
            "type": "string"
          },
          "twitch": {
            "type": "string"
          }
        }
      },
      "Page": {
        "type": "object",
        "properties": {
          "handle": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "bio": {
            "type": "string"
          },
          "avatar": {
            "type": "string"
          },
          "theme": {
            "type": "string",
            "enum": [
              "paper",
              "ink",
              "cobalt",
              "sage",
              "night"
            ]
          },
          "buttonStyle": {
            "type": "string",
            "enum": [
              "filled",
              "outline"
            ]
          },
          "links": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Link"
            }
          },
          "socials": {
            "type": "object",
            "properties": {
              "website": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "x": {
                "type": "string"
              },
              "instagram": {
                "type": "string"
              },
              "tiktok": {
                "type": "string"
              },
              "youtube": {
                "type": "string"
              },
              "linkedin": {
                "type": "string"
              },
              "github": {
                "type": "string"
              },
              "threads": {
                "type": "string"
              },
              "bluesky": {
                "type": "string"
              },
              "facebook": {
                "type": "string"
              },
              "whatsapp": {
                "type": "string"
              },
              "telegram": {
                "type": "string"
              },
              "spotify": {
                "type": "string"
              },
              "twitch": {
                "type": "string"
              }
            },
            "description": "Full addresses (email is the bare address)."
          },
          "showCredit": {
            "type": "boolean"
          },
          "views": {
            "type": "integer"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Stats": {
        "type": "object",
        "properties": {
          "handle": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "views": {
            "type": "integer"
          },
          "clicks": {
            "type": "integer"
          },
          "links": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Link"
                },
                {
                  "type": "object",
                  "properties": {
                    "clicks": {
                      "type": "integer"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    }
  }
}
