{"openapi":"3.1.0","info":{"title":"Liinks Public API","version":"1.0.0","description":"Programmatic access to Liinks profiles. Designed for both humans (curl, Postman) and AI agents.\n\n## Authentication\nGenerate an API key in the dashboard at **Settings → API Keys**. Pass it as a Bearer token:\n\n```\nAuthorization: Bearer liinks_live_<your-token>\n```\n\n## Quick start\n1. `GET /api/v1/me` — confirm authentication and read which account the key belongs to (id, slug, plan).\n2. `GET /api/v1/profiles` — list profiles you can manage. Your own profile is always included.\n3. `PATCH /api/v1/profiles/{profileId}` — set displayName, bio, slug.\n4. `PATCH /api/v1/profiles/{profileId}/styles` — set theme colors, fonts, background.\n5. `POST /api/v1/profiles/{profileId}/blocks` — add link or text blocks.\n6. `POST /api/v1/profiles/{profileId}/blocks/reorder` — set the display order.\n7. `POST /api/v1/profiles/{profileId}/screenshot` — render the live profile to JPEGs to verify what the changes look like.\n\n## Reading visitor activity\n- `GET /api/v1/profiles/{profileId}/pages` — list the profile's pages (the implicit `home` page plus any custom pages).\n- `GET /api/v1/profiles/{profileId}/form-submissions` — visitor responses to forms on the profile, newest first. Supports `?since=<iso>` for polling.\n- `GET /api/v1/profiles/{profileId}/subscribers` — email subscribers, newest first. Supports `?since=<iso>` for polling.\n\n## Plan requirements\n- An active paid subscription is required to use any endpoint (returns 402 otherwise).\n- `POST /api/v1/profiles` additionally requires a plan that allows managing multiple profiles.\n\n## Error format\nEvery error returns the same envelope: `{ \"error\": { \"code\", \"message\", \"details?\" } }`. Validation errors put per-field issues at `error.details.issues[]` so agents can self-correct.\n\n## Rate limits\n- 120 requests per minute per key (all verbs).\n- 30 mutations per minute per key (POST/PATCH/PUT/DELETE).\n\nEvery response advertises the policy in `RateLimit-Policy`; authenticated responses also carry `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` for the most constrained bucket, so clients can self-throttle in real time. On 429, wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying.\n\n## Versioning & deprecation\nThe API is versioned in the URL (`/api/v1`). Within a major version changes are additive: new fields and endpoints may appear, existing shapes and validation only change in a new major version. Version-less URLs (`/api/docs`, `/api/openapi.json`, `/api/mcp`) always point at the current stable version, and `GET /api` lists every version with its status. When a version is scheduled for retirement, its responses will carry `Deprecation` and `Sunset` headers at least 180 days before shutdown and API key owners are emailed. No version has ever been deprecated; v1 is current.\n\n## MCP server\nAI assistants can call this API natively over the Model Context Protocol: Streamable HTTP endpoint at `/api/mcp`, authenticated with the same Bearer API key as a header. Manifest: `/.well-known/mcp/manifest.json`; setup guide: [/developers](/developers).\n\n## Agent recipes\n\n### Edit the profile your API key owns\nThe common case — works on any paid plan, no multi-profile capability needed.\n1. `GET /api/v1/profiles` — the API key owner's own profile is always included. Capture its `id` from the first (and on a single-profile plan, only) entry.\n2. `PATCH /api/v1/profiles/{id}` with `{ \"displayName\": \"Jane Doe\", \"bio\": \"Photographer based in NYC\" }` — update profile fields. Pass `null` to clear `displayName`, `bio`, or `tagline`.\n3. `PATCH /api/v1/profiles/{id}/styles` with `{ \"buttonColor\": \"#1a1a1a\", \"background\": { \"backgroundType\": \"GRADIENT\", \"backgroundValue\": \"linear-gradient(180deg, #fef3e8 0%, #f5e6f3 100%)\" } }` — adjust theme colors, fonts, or background.\n4. `POST /api/v1/profiles/{id}/blocks` with `{ \"type\": \"link\", \"title\": \"Portfolio\", \"url\": \"https://example.com\" }`. Repeat per block. New blocks default to the top of the page; pass `?position=bottom` to append.\n5. (optional) `POST /api/v1/profiles/{id}/blocks/reorder` with `{ \"pageId\": \"home\", \"orderedIds\": [...] }` if you want a specific final order. The list MUST contain every block currently on the page.\n\n### Provision and populate a new managed profile\nRequires an API key whose owner has a multi-profile plan. For editing an existing profile, use the recipe above instead.\n1. `POST /api/v1/profiles` with `{ \"slug\": \"jane.doe\", \"displayName\": \"Jane Doe\", \"bio\": \"Photographer based in NYC\" }` — creates the managed profile and returns its `id`.\n2. `PATCH /api/v1/profiles/{id}/styles` with `{ \"buttonColor\": \"#1a1a1a\", \"background\": { \"backgroundType\": \"GRADIENT\", \"backgroundValue\": \"linear-gradient(180deg, #fef3e8 0%, #f5e6f3 100%)\" } }` — set the theme.\n3. `POST /api/v1/profiles/{id}/blocks` with `{ \"type\": \"link\", \"title\": \"Portfolio\", \"url\": \"https://example.com\" }`. Repeat per link. New blocks default to the top of the page; pass `?position=bottom` to append, or `?after=<blockId>` to slot beneath an existing block.\n4. `GET /api/v1/profiles/{id}/blocks?pageId=home` — fetch the current order to confirm.\n5. `POST /api/v1/profiles/{id}/blocks/reorder` with `{ \"pageId\": \"home\", \"orderedIds\": [...] }` — set the final order. The list MUST contain every block currently on the page.\n\n### Verify a change by screenshot\nAfter any mutation (profile fields, styles, blocks), capture the live render and read the images to confirm the result.\n1. Apply your changes via the relevant `PATCH` / `POST` endpoint(s).\n2. `POST /api/v1/profiles/{id}/screenshot` with `{}` — defaults to a 375×812 mobile capture, up to 10 paginated pages. Pass `{ \"viewportWidth\": 1280, \"viewportHeight\": 800 }` for the desktop layout.\n3. The response returns `pages[]` in top-to-bottom order, each with a public CDN `url`. Fetch the JPEGs and inspect them. If `truncated: true`, re-call with a higher `maxPages`.\n\n### Hide every block except one\n1. `GET /api/v1/profiles/{id}/blocks?pageId=home` — list the blocks on the page.\n2. For every returned block whose `id` is NOT the one you want to keep visible: `PATCH /api/v1/profiles/{id}/blocks/{blockId}` with `{ \"hidden\": true }`. Use `{ \"hidden\": false }` later to restore. Hiding does not delete; the block keeps its position.\n\n### Poll for new form submissions or subscribers\nGeneric pattern for syncing list endpoints that support `?since=`. Works the same for `/form-submissions` and `/subscribers`.\n1. On first run: `GET /api/v1/profiles/{id}/form-submissions?limit=50` (no `since`). Process the returned items, then store `latestSeen = max(item.createdAt)` and the set of `id`s you've already handled.\n2. On each subsequent run: `GET /api/v1/profiles/{id}/form-submissions?since=<latestSeen>`. The server returns only items with `createdAt > since`.\n3. Filter out any returned `id` already in your processed set (two items can share a millisecond — `since` is exclusive on time but `id`-level dedupe closes the gap). Process the rest, then update `latestSeen` to the new max.\n4. If a single response is at the `limit` size, run again immediately with the new `latestSeen` until you get a partial page — that indicates you've caught up. Mind the 120 req/min rate limit per key.\n\nBrowse this spec interactively at [/api/docs](/api/docs) (always points at the current version)."},"servers":[{"url":"/api/v1"}],"tags":[{"name":"Account","description":"The account that owns this API key. Use for connection-test flows that need a display label."},{"name":"Profiles","description":"Top-level user profiles."},{"name":"Styles","description":"Theme, fonts, background, and layout."},{"name":"Blocks","description":"Tappable links and text blocks rendered on the profile, in display order."},{"name":"Pages","description":"Pages on a profile. Returned page IDs are accepted by `?pageId=` on the blocks endpoints."},{"name":"Form submissions","description":"Visitor-submitted responses to forms on the profile. Designed for polling."},{"name":"Subscribers","description":"Email subscribers on the profile — auto-created from form submissions or added manually. Designed for polling."},{"name":"Screenshot","description":"Render the live profile to one or more JPEGs so a human or agent can verify what their changes look like in a browser."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key generated in the dashboard. Format: `liinks_live_<32-char-token>`."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["validation_error","unauthorized","plan_required","forbidden","not_found","conflict","rate_limited","server_error"],"description":"Machine-readable error category."},"message":{"type":"string","description":"Human-readable error summary."},"details":{"type":"object","additionalProperties":true,"description":"Code-specific extra context. For `validation_error`: `details.issues[]` lists each Zod issue (`path`, `code`, `message`). For `rate_limited`: `details.retryAfterSeconds`. For `plan_required`: `details.requiredCapability` when the plan lacks a capability, or `details.limit` and `details.current` when a plan quota (e.g. profile count) is exhausted."}}}}},"MeResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier for the API key owner. Matches the `id` of this user in `GET /profiles` (the API key owner's profile is always included there)."},"slug":{"type":"string","description":"URL handle — the public profile lives at `https://liinks.co/<slug>`. Lowercase, 3–40 characters, letters/numbers/period/underscore only."},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Account email. Used for login and billing. May be null for accounts created via social sign-in that never set one."},"displayName":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Name shown at the top of the profile. May be null if the user hasn't set one yet."},"plan":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Identifier of the subscription tier (e.g. `PREMIUM_2`, `TEAM_1`). `null` if the account has no active paid subscription. Treat this as opaque — exact values are not part of the public contract."}},"required":["id","slug","email","displayName","plan"],"additionalProperties":false}},"required":["user"],"additionalProperties":false},"ProfileResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"profile":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9._]{3,40}$","description":"URL handle — appears in https://liinks.co/<slug>. Globally unique. 3–40 characters, letters/numbers/period/underscore only (no hyphens). Case-insensitive: stored lowercase."},"displayName":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"null"}],"description":"Name shown at the top of the profile. Pass null to clear."},"bio":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"description":"Short biography shown beneath the display name. Plain text, up to 500 characters. Pass null to clear."},"tagline":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"description":"Optional one-liner shown under the bio. Up to 120 characters. Pass null to clear."},"profilePictureUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the profile picture. The server fetches and re-hosts the image; the response returns the rehosted URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF. SVG/ICO/BMP are rejected."},"id":{"type":"string"},"socials":{"maxItems":50,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["SPECIAL_DIVIDER","EMAIL","NEWSLETTER","PHONE","SMS","WEB","BUYMEACOFFEE","CASHAPP","GOFUNDME","PAYPAL","VENMO","AMAZON","APPLEMUSIC","APPLEPODCASTS","ARTSTATION","BANDCAMP","BEHANCE","BLUESKY","CAFFEINE","CLUBHOUSE","DISCORD","DRIBBBLE","DUOLINGO","FACEBOOK","GITHUB","GOODREADS","GOOGLEPODCASTS","INSTAGRAM","KOFI","LASTFM","LINKEDIN","MEDIUM","MEETUP","ONLYFANS","PATREON","PINTEREST","REDDIT","REDNOTE","SIGNAL","SLACK","SNAPCHAT","SOUNDCLOUD","SPOTIFY","STEAM","STRAVA","TELEGRAM","THREADS","TIDAL","TIKTOK","TUMBLR","TWITCH","TWITTER","UNSPLASH","VIMEO","WECHAT","WHATSAPP","YOUTUBE","YOUTUBEMUSIC","YOUTUBESHORTS"],"description":"Social platform / link kind. Common values: `INSTAGRAM`, `TIKTOK`, `YOUTUBE`, `TWITTER`, `LINKEDIN`, `FACEBOOK`, `THREADS`, `EMAIL`, `WEB`. The full list is auto-generated; reject with 400 if unknown."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"Destination for the social link. For most platforms a username (e.g. `youngfrikanna`) or a full URL both work — the renderer normalizes them. For `EMAIL` and `PHONE`, just the address/number."},"label":{"description":"Optional display label override. Most callers can omit this.","anyOf":[{"type":"string","maxLength":60},{"type":"null"}]}},"required":["type","url"],"additionalProperties":false},"description":"Row of social-platform icons rendered above the blocks (e.g. Instagram, TikTok, YouTube). On create, this is the initial list. After creation, manage socials via `PUT /profiles/{id}/socials` (which preserves per-type ids and click counts). Up to 50 entries."},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["slug","displayName","bio","tagline","profilePictureUrl","id","socials","createdAt","updatedAt"],"additionalProperties":false}},"required":["profile"],"additionalProperties":false},"ProfileCreateRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9._]{3,40}$","description":"URL handle — appears in https://liinks.co/<slug>. Globally unique. 3–40 characters, letters/numbers/period/underscore only (no hyphens). Case-insensitive: stored lowercase."},"displayName":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"null"}],"description":"Name shown at the top of the profile. Pass null to clear."},"bio":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"description":"Short biography shown beneath the display name. Plain text, up to 500 characters. Pass null to clear."},"tagline":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"description":"Optional one-liner shown under the bio. Up to 120 characters. Pass null to clear."},"profilePictureUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the profile picture. The server fetches and re-hosts the image; the response returns the rehosted URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF. SVG/ICO/BMP are rejected."},"socials":{"maxItems":50,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["SPECIAL_DIVIDER","EMAIL","NEWSLETTER","PHONE","SMS","WEB","BUYMEACOFFEE","CASHAPP","GOFUNDME","PAYPAL","VENMO","AMAZON","APPLEMUSIC","APPLEPODCASTS","ARTSTATION","BANDCAMP","BEHANCE","BLUESKY","CAFFEINE","CLUBHOUSE","DISCORD","DRIBBBLE","DUOLINGO","FACEBOOK","GITHUB","GOODREADS","GOOGLEPODCASTS","INSTAGRAM","KOFI","LASTFM","LINKEDIN","MEDIUM","MEETUP","ONLYFANS","PATREON","PINTEREST","REDDIT","REDNOTE","SIGNAL","SLACK","SNAPCHAT","SOUNDCLOUD","SPOTIFY","STEAM","STRAVA","TELEGRAM","THREADS","TIDAL","TIKTOK","TUMBLR","TWITCH","TWITTER","UNSPLASH","VIMEO","WECHAT","WHATSAPP","YOUTUBE","YOUTUBEMUSIC","YOUTUBESHORTS"],"description":"Social platform / link kind. Common values: `INSTAGRAM`, `TIKTOK`, `YOUTUBE`, `TWITTER`, `LINKEDIN`, `FACEBOOK`, `THREADS`, `EMAIL`, `WEB`. The full list is auto-generated; reject with 400 if unknown."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"Destination for the social link. For most platforms a username (e.g. `youngfrikanna`) or a full URL both work — the renderer normalizes them. For `EMAIL` and `PHONE`, just the address/number."},"label":{"description":"Optional display label override. Most callers can omit this.","anyOf":[{"type":"string","maxLength":60},{"type":"null"}]}},"required":["type","url"],"additionalProperties":false},"description":"Row of social-platform icons rendered above the blocks (e.g. Instagram, TikTok, YouTube). On create, this is the initial list. After creation, manage socials via `PUT /profiles/{id}/socials` (which preserves per-type ids and click counts). Up to 50 entries."},"prospect":{"description":"Create this profile as a prospect preview for the \"claim your profile\" outreach flow. The response includes tokenized claim/takedown URLs.","type":"object","properties":{"email":{"description":"Outreach email address for the prospect. Stored for suppression/dedupe; never rendered on the profile.","type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"source":{"description":"Sourcing tag for funnel attribution, e.g. \"photographers/dork/austin\". Survives the claim for cohort analysis.","type":"string","maxLength":100},"expiresInDays":{"description":"Days until the unclaimed preview is automatically taken down. Defaults to 45.","type":"integer","minimum":1,"maximum":180}},"additionalProperties":false}},"required":["slug"],"additionalProperties":false},"ProfileCreateResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"profile":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9._]{3,40}$","description":"URL handle — appears in https://liinks.co/<slug>. Globally unique. 3–40 characters, letters/numbers/period/underscore only (no hyphens). Case-insensitive: stored lowercase."},"displayName":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"null"}],"description":"Name shown at the top of the profile. Pass null to clear."},"bio":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"description":"Short biography shown beneath the display name. Plain text, up to 500 characters. Pass null to clear."},"tagline":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"description":"Optional one-liner shown under the bio. Up to 120 characters. Pass null to clear."},"profilePictureUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the profile picture. The server fetches and re-hosts the image; the response returns the rehosted URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF. SVG/ICO/BMP are rejected."},"id":{"type":"string"},"socials":{"maxItems":50,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["SPECIAL_DIVIDER","EMAIL","NEWSLETTER","PHONE","SMS","WEB","BUYMEACOFFEE","CASHAPP","GOFUNDME","PAYPAL","VENMO","AMAZON","APPLEMUSIC","APPLEPODCASTS","ARTSTATION","BANDCAMP","BEHANCE","BLUESKY","CAFFEINE","CLUBHOUSE","DISCORD","DRIBBBLE","DUOLINGO","FACEBOOK","GITHUB","GOODREADS","GOOGLEPODCASTS","INSTAGRAM","KOFI","LASTFM","LINKEDIN","MEDIUM","MEETUP","ONLYFANS","PATREON","PINTEREST","REDDIT","REDNOTE","SIGNAL","SLACK","SNAPCHAT","SOUNDCLOUD","SPOTIFY","STEAM","STRAVA","TELEGRAM","THREADS","TIDAL","TIKTOK","TUMBLR","TWITCH","TWITTER","UNSPLASH","VIMEO","WECHAT","WHATSAPP","YOUTUBE","YOUTUBEMUSIC","YOUTUBESHORTS"],"description":"Social platform / link kind. Common values: `INSTAGRAM`, `TIKTOK`, `YOUTUBE`, `TWITTER`, `LINKEDIN`, `FACEBOOK`, `THREADS`, `EMAIL`, `WEB`. The full list is auto-generated; reject with 400 if unknown."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"Destination for the social link. For most platforms a username (e.g. `youngfrikanna`) or a full URL both work — the renderer normalizes them. For `EMAIL` and `PHONE`, just the address/number."},"label":{"description":"Optional display label override. Most callers can omit this.","anyOf":[{"type":"string","maxLength":60},{"type":"null"}]}},"required":["type","url"],"additionalProperties":false},"description":"Row of social-platform icons rendered above the blocks (e.g. Instagram, TikTok, YouTube). On create, this is the initial list. After creation, manage socials via `PUT /profiles/{id}/socials` (which preserves per-type ids and click counts). Up to 50 entries."},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["slug","displayName","bio","tagline","profilePictureUrl","id","socials","createdAt","updatedAt"],"additionalProperties":false},"claim":{"description":"Present only when the request included `prospect`.","type":"object","properties":{"claimUrl":{"type":"string","description":"Tokenized URL the prospect uses to claim the profile."},"takedownUrl":{"type":"string","description":"Tokenized URL that lets the prospect remove the preview without logging in."},"expiresAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When the unclaimed preview is automatically taken down."}},"required":["claimUrl","takedownUrl","expiresAt"],"additionalProperties":false}},"required":["profile"],"additionalProperties":false},"ProfileUpdateRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9._]{3,40}$","description":"URL handle — appears in https://liinks.co/<slug>. Globally unique. 3–40 characters, letters/numbers/period/underscore only (no hyphens). Case-insensitive: stored lowercase."},"displayName":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"null"}],"description":"Name shown at the top of the profile. Pass null to clear."},"bio":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"description":"Short biography shown beneath the display name. Plain text, up to 500 characters. Pass null to clear."},"tagline":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"description":"Optional one-liner shown under the bio. Up to 120 characters. Pass null to clear."},"profilePictureUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the profile picture. The server fetches and re-hosts the image; the response returns the rehosted URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF. SVG/ICO/BMP are rejected."}},"additionalProperties":false},"ProfilesListResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"profiles":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9._]{3,40}$","description":"URL handle — appears in https://liinks.co/<slug>. Globally unique. 3–40 characters, letters/numbers/period/underscore only (no hyphens). Case-insensitive: stored lowercase."},"displayName":{"anyOf":[{"type":"string","minLength":1,"maxLength":120},{"type":"null"}],"description":"Name shown at the top of the profile. Pass null to clear."},"bio":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"description":"Short biography shown beneath the display name. Plain text, up to 500 characters. Pass null to clear."},"tagline":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}],"description":"Optional one-liner shown under the bio. Up to 120 characters. Pass null to clear."},"profilePictureUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the profile picture. The server fetches and re-hosts the image; the response returns the rehosted URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF. SVG/ICO/BMP are rejected."},"id":{"type":"string"},"socials":{"maxItems":50,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["SPECIAL_DIVIDER","EMAIL","NEWSLETTER","PHONE","SMS","WEB","BUYMEACOFFEE","CASHAPP","GOFUNDME","PAYPAL","VENMO","AMAZON","APPLEMUSIC","APPLEPODCASTS","ARTSTATION","BANDCAMP","BEHANCE","BLUESKY","CAFFEINE","CLUBHOUSE","DISCORD","DRIBBBLE","DUOLINGO","FACEBOOK","GITHUB","GOODREADS","GOOGLEPODCASTS","INSTAGRAM","KOFI","LASTFM","LINKEDIN","MEDIUM","MEETUP","ONLYFANS","PATREON","PINTEREST","REDDIT","REDNOTE","SIGNAL","SLACK","SNAPCHAT","SOUNDCLOUD","SPOTIFY","STEAM","STRAVA","TELEGRAM","THREADS","TIDAL","TIKTOK","TUMBLR","TWITCH","TWITTER","UNSPLASH","VIMEO","WECHAT","WHATSAPP","YOUTUBE","YOUTUBEMUSIC","YOUTUBESHORTS"],"description":"Social platform / link kind. Common values: `INSTAGRAM`, `TIKTOK`, `YOUTUBE`, `TWITTER`, `LINKEDIN`, `FACEBOOK`, `THREADS`, `EMAIL`, `WEB`. The full list is auto-generated; reject with 400 if unknown."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"Destination for the social link. For most platforms a username (e.g. `youngfrikanna`) or a full URL both work — the renderer normalizes them. For `EMAIL` and `PHONE`, just the address/number."},"label":{"description":"Optional display label override. Most callers can omit this.","anyOf":[{"type":"string","maxLength":60},{"type":"null"}]}},"required":["type","url"],"additionalProperties":false},"description":"Row of social-platform icons rendered above the blocks (e.g. Instagram, TikTok, YouTube). On create, this is the initial list. After creation, manage socials via `PUT /profiles/{id}/socials` (which preserves per-type ids and click counts). Up to 50 entries."},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["slug","displayName","bio","tagline","profilePictureUrl","id","socials","createdAt","updatedAt"],"additionalProperties":false}}},"required":["profiles"],"additionalProperties":false},"StylesUpdateRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"buttonColor":{"description":"Background color of standard button-layout link blocks. Ignored when `tactileLinkType` is not `NONE` — tactile rendering derives the fill from the page color (or `sheetColor` on non-`CLASSIC` layouts) and overrides this value.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"buttonTextColor":{"description":"Foreground (text) color of standard button-layout link blocks.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"labelColor":{"description":"Background color of label-style accents (e.g. CTA pills, in-block tags).","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"labelTextColor":{"description":"Foreground color used inside labels.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"ctaColor":{"description":"Background color of primary call-to-action elements.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"ctaTextColor":{"description":"Foreground color of primary call-to-action elements.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"foregroundColor":{"description":"Default text/icon color used across the page.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"sheetColor":{"description":"Background color of the central content sheet on banner/business header layouts.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"headerTextColor":{"description":"Color of header text (display name, bio).","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"borderColor":{"description":"Color used for borders on bordered button styles. Ignored when `tactileLinkType` is not `NONE` — tactile rendering supplies its own border color.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"headerLayout":{"description":"Top-of-profile layout. `CLASSIC` shows a small circular avatar centered above the name. `HEADSHOT` displays the `profilePictureUrl` as a full-width banner across the top of the page (use this when your profile picture is a horizontal lifestyle/portrait shot). `BANNER` shows the page `background` at the top and the avatar as a circle on a sheet beneath (use this when you want a colored/gradient/animated banner instead of a photo). `BUSINESS` is a logo + tagline layout, also rendering the profile picture as a circle. When in doubt, start with `HEADSHOT` for personal/creator profiles or `CLASSIC` for the simplest look. Note: when `tactileLinkType` is not `NONE`, `CLASSIC` requires `background.backgroundType` to be `SOLID` — tactile buttons inherit the page background on this layout, so a deterministic color is required.","anyOf":[{"type":"string","enum":["CLASSIC","BANNER","BUSINESS","HEADSHOT"]},{"type":"null"}]},"fontStyle1":{"description":"Primary font (used for the display name and prominent text). Resolves against Google Fonts.","anyOf":[{"type":"object","properties":{"family":{"description":"Google Fonts family name. One of: Open Sans, Space Mono, Work Sans, Inter, Rubik, Libre Franklin, Alegreya Sans, Alegreya, Chivo, Source Sans Pro, Roboto, Roboto Mono, Roboto Slab, Poppins, Archivo Narrow, Libre Baskerville, Karla, Lora, Proza Libre, Spectral, IBM Plex Sans, Crimson Text, PT Sans, PT Serif, Lato, Cardo, Neuton, Cabin, Anonymous Pro, Raleway, Arvo, Merriweather, Varela Round. Pass null to clear.","anyOf":[{"type":"string","enum":["Open Sans","Space Mono","Work Sans","Inter","Rubik","Libre Franklin","Alegreya Sans","Alegreya","Chivo","Source Sans Pro","Roboto","Roboto Mono","Roboto Slab","Poppins","Archivo Narrow","Libre Baskerville","Karla","Lora","Proza Libre","Spectral","IBM Plex Sans","Crimson Text","PT Sans","PT Serif","Lato","Cardo","Neuton","Cabin","Anonymous Pro","Raleway","Arvo","Merriweather","Varela Round"]},{"type":"null"}]},"style":{"description":"Font weight specifier. `400` is regular, `700` is bold; suffix `i` for italic (e.g. `400i`, `700i`). Available weights vary by family — see family-specific options in the schema. Defaults to `400` when omitted.","anyOf":[{"type":"string","pattern":"^[1-9]00i?$"},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"fontStyle2":{"description":"Secondary font (used for body copy and block labels). Resolves against Google Fonts.","anyOf":[{"type":"object","properties":{"family":{"description":"Google Fonts family name. One of: Open Sans, Space Mono, Work Sans, Inter, Rubik, Libre Franklin, Alegreya Sans, Alegreya, Chivo, Source Sans Pro, Roboto, Roboto Mono, Roboto Slab, Poppins, Archivo Narrow, Libre Baskerville, Karla, Lora, Proza Libre, Spectral, IBM Plex Sans, Crimson Text, PT Sans, PT Serif, Lato, Cardo, Neuton, Cabin, Anonymous Pro, Raleway, Arvo, Merriweather, Varela Round. Pass null to clear.","anyOf":[{"type":"string","enum":["Open Sans","Space Mono","Work Sans","Inter","Rubik","Libre Franklin","Alegreya Sans","Alegreya","Chivo","Source Sans Pro","Roboto","Roboto Mono","Roboto Slab","Poppins","Archivo Narrow","Libre Baskerville","Karla","Lora","Proza Libre","Spectral","IBM Plex Sans","Crimson Text","PT Sans","PT Serif","Lato","Cardo","Neuton","Cabin","Anonymous Pro","Raleway","Arvo","Merriweather","Varela Round"]},{"type":"null"}]},"style":{"description":"Font weight specifier. `400` is regular, `700` is bold; suffix `i` for italic (e.g. `400i`, `700i`). Available weights vary by family — see family-specific options in the schema. Defaults to `400` when omitted.","anyOf":[{"type":"string","pattern":"^[1-9]00i?$"},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"background":{"description":"The page background. Renders on mobile and inside the centered profile column on desktop; it is also echoed (frozen) as the desktop chrome around the column unless `desktopBackground` overrides that chrome with a solid color. When `tactileLinkType` is not `NONE` and `headerLayout` is `CLASSIC`, only `backgroundType: \"SOLID\"` is accepted — non-solid backgrounds are rejected with a 400 because tactile buttons on the CLASSIC layout derive their fill from the page background.","anyOf":[{"type":"object","properties":{"backgroundType":{"description":"Background variant. `NONE` (transparent — page sheet color shows through), `SOLID` (single color), `GRADIENT` (CSS multi-stop gradient), `SPLIT` (hard horizontal/vertical split — implemented as a hard-stop gradient), `IMAGE` (uses the upload set via the top-level `backgroundImageUrl` field), or `ANIMATED` (WebGL shader).","anyOf":[{"type":"string","enum":["SOLID","GRADIENT","SPLIT","IMAGE","ANIMATED","NONE"]},{"type":"null"}]},"backgroundValue":{"description":"Renderer-interpreted value, format depends on `backgroundType`:\n- `NONE`: ignored.\n- `SOLID`: a CSS color — hex (`#ff5722`), `rgb(...)`, `rgba(...)`, `hsl(...)`, or named.\n- `GRADIENT`: a raw CSS gradient string, e.g. `linear-gradient(180deg, #aaa 0%, #bbb 100%)` or `radial-gradient(...)`. Anything valid in CSS `background:` works.\n- `SPLIT`: a hard-stop CSS gradient producing the split effect, e.g. `linear-gradient(180deg, #aaa 0%, #aaa 50%, #bbb 50%, #bbb 100%)`.\n- `IMAGE`: ignored — set the actual image via the top-level `backgroundImageUrl` field on this styles object.\n- `ANIMATED`: a JSON-encoded shader config, e.g. `{\"shader\":\"meshGradient\",\"colors\":[\"#7ed0ff\",\"#b8d4ed\",\"#9b8ae3\",\"#f2f8f8\"],\"speed\":0.1,\"scale\":1,\"intensity\":0.4,\"softness\":0.5}`. Available shaders include `meshGradient`; supply 2–6 colors as hex/rgba strings.","anyOf":[{"type":"string","maxLength":4000},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"desktopBackground":{"description":"Optional override for the desktop chrome — the area around the centered profile column on desktop. Only `SOLID` and `NONE` are supported. Set `backgroundType: \"NONE\"` (or omit) to show a frozen copy of the mobile `background` there (or a dimmed `sheetColor` on the `HEADSHOT` layout); set `SOLID` with a `backgroundValue` color to paint the chrome that color. The profile column itself always renders the mobile `background`.","anyOf":[{"type":"object","properties":{"backgroundType":{"description":"Variant for the desktop chrome — the area AROUND the centered profile column on desktop. The column itself always renders the mobile `background` (any variant, image included). Only `SOLID` (a single color) or `NONE` (no override — the chrome shows a frozen copy of the mobile `background`, or a dimmed `sheetColor` on the `HEADSHOT` layout) are accepted here.","anyOf":[{"type":"string","enum":["NONE","SOLID"]},{"type":"null"}]},"backgroundValue":{"description":"CSS color when `backgroundType` is `SOLID` (hex / `rgb(...)` / `hsl(...)` / named). Ignored when `backgroundType` is `NONE`.","anyOf":[{"type":"string","maxLength":4000},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"backgroundImageUrl":{"description":"Public URL of an image to use as the page background. Set this AND `background.backgroundType: \"IMAGE\"` to render it. The server fetches and re-hosts the image; the response returns the rehosted CDN URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF.","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"shadowRatio":{"description":"Shadow intensity for buttons and blocks. 0 = no shadow, 1 = strongest. Range: [0, 1]. Ignored when `tactileLinkType` is not `NONE` — tactile rendering supplies its own shadow.","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"borderRadiusRatio":{"description":"Corner roundness for buttons and blocks. 0 = square, 1 = pill. Range: [0, 1].","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"borderWidthRatio":{"description":"Border thickness for bordered button styles. 0 = no border, 1 = thickest. Range: [0, 1]. Ignored when `tactileLinkType` is not `NONE` — tactile rendering supplies its own border width.","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"spacingRatio":{"description":"Vertical spacing between blocks. 0 = tightest, 1 = loosest. Range: [0, 1].","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"enableSheetFade":{"description":"When true, renders a soft gradient fade between the banner/headshot image and the sheet area below it (avoiding a hard edge). Only applies to non-`CLASSIC` headerLayouts that show a sheet. Defaults to false.","anyOf":[{"type":"boolean"},{"type":"null"}]},"enableSearch":{"description":"When true, shows a search icon in the top-right corner that filters/highlights blocks by typed text. Useful for profiles with many blocks (gallery, store). Defaults to false.","anyOf":[{"type":"boolean"},{"type":"null"}]},"tactileLinkType":{"description":"Button surface style for `button`-layout link blocks. `NONE` (default) renders a plain button using the explicit color/border/shadow fields. `FLAT` renders a flat tactile fill with a soft outer shadow. `CONVEX` adds a subtle raised highlight (button looks pushed up). `CONCAVE` adds an inset shadow at the top (button looks pressed in). `INSET` renders the button as a recessed slot in the page. `FROSTED_GLASS` renders the button as a translucent, frosted surface over a blurred, saturation-boosted backdrop. When set to anything other than `NONE`, the renderer derives button fill, border color, border width, and shadow from the page background (or the `sheetColor` on non-`CLASSIC` layouts), and **overrides** the explicit `buttonColor`, `borderColor`, `borderWidthRatio`, and `shadowRatio` values. Additional constraint: on the `CLASSIC` header layout, tactile buttons inherit from the page background, so `background.backgroundType` must be `SOLID` whenever `tactileLinkType` is set to `FLAT`, `CONVEX`, `CONCAVE`, or `INSET`. Combining `CLASSIC` + one of those tactile types + a non-`SOLID` background is rejected with a 400. `FROSTED_GLASS` is exempt — it renders its own translucent surface and works over any background type.","anyOf":[{"type":"string","enum":["NONE","FLAT","CONCAVE","CONVEX","INSET","FROSTED_GLASS"]},{"type":"null"}]}},"additionalProperties":false},"StylesResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"styles":{"type":"object","properties":{"buttonColor":{"description":"Background color of standard button-layout link blocks. Ignored when `tactileLinkType` is not `NONE` — tactile rendering derives the fill from the page color (or `sheetColor` on non-`CLASSIC` layouts) and overrides this value.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"buttonTextColor":{"description":"Foreground (text) color of standard button-layout link blocks.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"labelColor":{"description":"Background color of label-style accents (e.g. CTA pills, in-block tags).","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"labelTextColor":{"description":"Foreground color used inside labels.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"ctaColor":{"description":"Background color of primary call-to-action elements.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"ctaTextColor":{"description":"Foreground color of primary call-to-action elements.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"foregroundColor":{"description":"Default text/icon color used across the page.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"sheetColor":{"description":"Background color of the central content sheet on banner/business header layouts.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"headerTextColor":{"description":"Color of header text (display name, bio).","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"borderColor":{"description":"Color used for borders on bordered button styles. Ignored when `tactileLinkType` is not `NONE` — tactile rendering supplies its own border color.","anyOf":[{"type":"string","pattern":"^(?:#[0-9a-fA-F]{3,8}|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\\([^)]+\\)|[a-zA-Z]+)$"},{"type":"null"}]},"headerLayout":{"description":"Top-of-profile layout. `CLASSIC` shows a small circular avatar centered above the name. `HEADSHOT` displays the `profilePictureUrl` as a full-width banner across the top of the page (use this when your profile picture is a horizontal lifestyle/portrait shot). `BANNER` shows the page `background` at the top and the avatar as a circle on a sheet beneath (use this when you want a colored/gradient/animated banner instead of a photo). `BUSINESS` is a logo + tagline layout, also rendering the profile picture as a circle. When in doubt, start with `HEADSHOT` for personal/creator profiles or `CLASSIC` for the simplest look. Note: when `tactileLinkType` is not `NONE`, `CLASSIC` requires `background.backgroundType` to be `SOLID` — tactile buttons inherit the page background on this layout, so a deterministic color is required.","anyOf":[{"type":"string","enum":["CLASSIC","BANNER","BUSINESS","HEADSHOT"]},{"type":"null"}]},"fontStyle1":{"description":"Primary font (used for the display name and prominent text). Resolves against Google Fonts.","anyOf":[{"type":"object","properties":{"family":{"description":"Google Fonts family name. One of: Open Sans, Space Mono, Work Sans, Inter, Rubik, Libre Franklin, Alegreya Sans, Alegreya, Chivo, Source Sans Pro, Roboto, Roboto Mono, Roboto Slab, Poppins, Archivo Narrow, Libre Baskerville, Karla, Lora, Proza Libre, Spectral, IBM Plex Sans, Crimson Text, PT Sans, PT Serif, Lato, Cardo, Neuton, Cabin, Anonymous Pro, Raleway, Arvo, Merriweather, Varela Round. Pass null to clear.","anyOf":[{"type":"string","enum":["Open Sans","Space Mono","Work Sans","Inter","Rubik","Libre Franklin","Alegreya Sans","Alegreya","Chivo","Source Sans Pro","Roboto","Roboto Mono","Roboto Slab","Poppins","Archivo Narrow","Libre Baskerville","Karla","Lora","Proza Libre","Spectral","IBM Plex Sans","Crimson Text","PT Sans","PT Serif","Lato","Cardo","Neuton","Cabin","Anonymous Pro","Raleway","Arvo","Merriweather","Varela Round"]},{"type":"null"}]},"style":{"description":"Font weight specifier. `400` is regular, `700` is bold; suffix `i` for italic (e.g. `400i`, `700i`). Available weights vary by family — see family-specific options in the schema. Defaults to `400` when omitted.","anyOf":[{"type":"string","pattern":"^[1-9]00i?$"},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"fontStyle2":{"description":"Secondary font (used for body copy and block labels). Resolves against Google Fonts.","anyOf":[{"type":"object","properties":{"family":{"description":"Google Fonts family name. One of: Open Sans, Space Mono, Work Sans, Inter, Rubik, Libre Franklin, Alegreya Sans, Alegreya, Chivo, Source Sans Pro, Roboto, Roboto Mono, Roboto Slab, Poppins, Archivo Narrow, Libre Baskerville, Karla, Lora, Proza Libre, Spectral, IBM Plex Sans, Crimson Text, PT Sans, PT Serif, Lato, Cardo, Neuton, Cabin, Anonymous Pro, Raleway, Arvo, Merriweather, Varela Round. Pass null to clear.","anyOf":[{"type":"string","enum":["Open Sans","Space Mono","Work Sans","Inter","Rubik","Libre Franklin","Alegreya Sans","Alegreya","Chivo","Source Sans Pro","Roboto","Roboto Mono","Roboto Slab","Poppins","Archivo Narrow","Libre Baskerville","Karla","Lora","Proza Libre","Spectral","IBM Plex Sans","Crimson Text","PT Sans","PT Serif","Lato","Cardo","Neuton","Cabin","Anonymous Pro","Raleway","Arvo","Merriweather","Varela Round"]},{"type":"null"}]},"style":{"description":"Font weight specifier. `400` is regular, `700` is bold; suffix `i` for italic (e.g. `400i`, `700i`). Available weights vary by family — see family-specific options in the schema. Defaults to `400` when omitted.","anyOf":[{"type":"string","pattern":"^[1-9]00i?$"},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"background":{"description":"The page background. Renders on mobile and inside the centered profile column on desktop; it is also echoed (frozen) as the desktop chrome around the column unless `desktopBackground` overrides that chrome with a solid color. When `tactileLinkType` is not `NONE` and `headerLayout` is `CLASSIC`, only `backgroundType: \"SOLID\"` is accepted — non-solid backgrounds are rejected with a 400 because tactile buttons on the CLASSIC layout derive their fill from the page background.","anyOf":[{"type":"object","properties":{"backgroundType":{"description":"Background variant. `NONE` (transparent — page sheet color shows through), `SOLID` (single color), `GRADIENT` (CSS multi-stop gradient), `SPLIT` (hard horizontal/vertical split — implemented as a hard-stop gradient), `IMAGE` (uses the upload set via the top-level `backgroundImageUrl` field), or `ANIMATED` (WebGL shader).","anyOf":[{"type":"string","enum":["SOLID","GRADIENT","SPLIT","IMAGE","ANIMATED","NONE"]},{"type":"null"}]},"backgroundValue":{"description":"Renderer-interpreted value, format depends on `backgroundType`:\n- `NONE`: ignored.\n- `SOLID`: a CSS color — hex (`#ff5722`), `rgb(...)`, `rgba(...)`, `hsl(...)`, or named.\n- `GRADIENT`: a raw CSS gradient string, e.g. `linear-gradient(180deg, #aaa 0%, #bbb 100%)` or `radial-gradient(...)`. Anything valid in CSS `background:` works.\n- `SPLIT`: a hard-stop CSS gradient producing the split effect, e.g. `linear-gradient(180deg, #aaa 0%, #aaa 50%, #bbb 50%, #bbb 100%)`.\n- `IMAGE`: ignored — set the actual image via the top-level `backgroundImageUrl` field on this styles object.\n- `ANIMATED`: a JSON-encoded shader config, e.g. `{\"shader\":\"meshGradient\",\"colors\":[\"#7ed0ff\",\"#b8d4ed\",\"#9b8ae3\",\"#f2f8f8\"],\"speed\":0.1,\"scale\":1,\"intensity\":0.4,\"softness\":0.5}`. Available shaders include `meshGradient`; supply 2–6 colors as hex/rgba strings.","anyOf":[{"type":"string","maxLength":4000},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"desktopBackground":{"description":"Optional override for the desktop chrome — the area around the centered profile column on desktop. Only `SOLID` and `NONE` are supported. Set `backgroundType: \"NONE\"` (or omit) to show a frozen copy of the mobile `background` there (or a dimmed `sheetColor` on the `HEADSHOT` layout); set `SOLID` with a `backgroundValue` color to paint the chrome that color. The profile column itself always renders the mobile `background`.","anyOf":[{"type":"object","properties":{"backgroundType":{"description":"Variant for the desktop chrome — the area AROUND the centered profile column on desktop. The column itself always renders the mobile `background` (any variant, image included). Only `SOLID` (a single color) or `NONE` (no override — the chrome shows a frozen copy of the mobile `background`, or a dimmed `sheetColor` on the `HEADSHOT` layout) are accepted here.","anyOf":[{"type":"string","enum":["NONE","SOLID"]},{"type":"null"}]},"backgroundValue":{"description":"CSS color when `backgroundType` is `SOLID` (hex / `rgb(...)` / `hsl(...)` / named). Ignored when `backgroundType` is `NONE`.","anyOf":[{"type":"string","maxLength":4000},{"type":"null"}]}},"additionalProperties":false},{"type":"null"}]},"backgroundImageUrl":{"description":"Public URL of an image to use as the page background. Set this AND `background.backgroundType: \"IMAGE\"` to render it. The server fetches and re-hosts the image; the response returns the rehosted CDN URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF.","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"shadowRatio":{"description":"Shadow intensity for buttons and blocks. 0 = no shadow, 1 = strongest. Range: [0, 1]. Ignored when `tactileLinkType` is not `NONE` — tactile rendering supplies its own shadow.","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"borderRadiusRatio":{"description":"Corner roundness for buttons and blocks. 0 = square, 1 = pill. Range: [0, 1].","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"borderWidthRatio":{"description":"Border thickness for bordered button styles. 0 = no border, 1 = thickest. Range: [0, 1]. Ignored when `tactileLinkType` is not `NONE` — tactile rendering supplies its own border width.","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"spacingRatio":{"description":"Vertical spacing between blocks. 0 = tightest, 1 = loosest. Range: [0, 1].","anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"enableSheetFade":{"description":"When true, renders a soft gradient fade between the banner/headshot image and the sheet area below it (avoiding a hard edge). Only applies to non-`CLASSIC` headerLayouts that show a sheet. Defaults to false.","anyOf":[{"type":"boolean"},{"type":"null"}]},"enableSearch":{"description":"When true, shows a search icon in the top-right corner that filters/highlights blocks by typed text. Useful for profiles with many blocks (gallery, store). Defaults to false.","anyOf":[{"type":"boolean"},{"type":"null"}]},"tactileLinkType":{"description":"Button surface style for `button`-layout link blocks. `NONE` (default) renders a plain button using the explicit color/border/shadow fields. `FLAT` renders a flat tactile fill with a soft outer shadow. `CONVEX` adds a subtle raised highlight (button looks pushed up). `CONCAVE` adds an inset shadow at the top (button looks pressed in). `INSET` renders the button as a recessed slot in the page. `FROSTED_GLASS` renders the button as a translucent, frosted surface over a blurred, saturation-boosted backdrop. When set to anything other than `NONE`, the renderer derives button fill, border color, border width, and shadow from the page background (or the `sheetColor` on non-`CLASSIC` layouts), and **overrides** the explicit `buttonColor`, `borderColor`, `borderWidthRatio`, and `shadowRatio` values. Additional constraint: on the `CLASSIC` header layout, tactile buttons inherit from the page background, so `background.backgroundType` must be `SOLID` whenever `tactileLinkType` is set to `FLAT`, `CONVEX`, `CONCAVE`, or `INSET`. Combining `CLASSIC` + one of those tactile types + a non-`SOLID` background is rejected with a 400. `FROSTED_GLASS` is exempt — it renders its own translucent surface and works over any background type.","anyOf":[{"type":"string","enum":["NONE","FLAT","CONCAVE","CONVEX","INSET","FROSTED_GLASS"]},{"type":"null"}]}},"additionalProperties":false}},"required":["styles"],"additionalProperties":false},"SocialsSetRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"socials":{"maxItems":50,"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["SPECIAL_DIVIDER","EMAIL","NEWSLETTER","PHONE","SMS","WEB","BUYMEACOFFEE","CASHAPP","GOFUNDME","PAYPAL","VENMO","AMAZON","APPLEMUSIC","APPLEPODCASTS","ARTSTATION","BANDCAMP","BEHANCE","BLUESKY","CAFFEINE","CLUBHOUSE","DISCORD","DRIBBBLE","DUOLINGO","FACEBOOK","GITHUB","GOODREADS","GOOGLEPODCASTS","INSTAGRAM","KOFI","LASTFM","LINKEDIN","MEDIUM","MEETUP","ONLYFANS","PATREON","PINTEREST","REDDIT","REDNOTE","SIGNAL","SLACK","SNAPCHAT","SOUNDCLOUD","SPOTIFY","STEAM","STRAVA","TELEGRAM","THREADS","TIDAL","TIKTOK","TUMBLR","TWITCH","TWITTER","UNSPLASH","VIMEO","WECHAT","WHATSAPP","YOUTUBE","YOUTUBEMUSIC","YOUTUBESHORTS"],"description":"Social platform / link kind. Common values: `INSTAGRAM`, `TIKTOK`, `YOUTUBE`, `TWITTER`, `LINKEDIN`, `FACEBOOK`, `THREADS`, `EMAIL`, `WEB`. The full list is auto-generated; reject with 400 if unknown."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"Destination for the social link. For most platforms a username (e.g. `youngfrikanna`) or a full URL both work — the renderer normalizes them. For `EMAIL` and `PHONE`, just the address/number."},"label":{"description":"Optional display label override. Most callers can omit this.","anyOf":[{"type":"string","maxLength":60},{"type":"null"}]}},"required":["type","url"],"additionalProperties":false},"description":"The complete social row, in display order (first = leftmost). Replaces the existing list — omit an entry to remove it, reorder the array to reorder the row. Per-type ids and click/view counts are preserved, so editing or reordering keeps each platform’s analytics intact."}},"required":["socials"],"additionalProperties":false},"SocialsResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"socials":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["SPECIAL_DIVIDER","EMAIL","NEWSLETTER","PHONE","SMS","WEB","BUYMEACOFFEE","CASHAPP","GOFUNDME","PAYPAL","VENMO","AMAZON","APPLEMUSIC","APPLEPODCASTS","ARTSTATION","BANDCAMP","BEHANCE","BLUESKY","CAFFEINE","CLUBHOUSE","DISCORD","DRIBBBLE","DUOLINGO","FACEBOOK","GITHUB","GOODREADS","GOOGLEPODCASTS","INSTAGRAM","KOFI","LASTFM","LINKEDIN","MEDIUM","MEETUP","ONLYFANS","PATREON","PINTEREST","REDDIT","REDNOTE","SIGNAL","SLACK","SNAPCHAT","SOUNDCLOUD","SPOTIFY","STEAM","STRAVA","TELEGRAM","THREADS","TIDAL","TIKTOK","TUMBLR","TWITCH","TWITTER","UNSPLASH","VIMEO","WECHAT","WHATSAPP","YOUTUBE","YOUTUBEMUSIC","YOUTUBESHORTS"],"description":"Social platform / link kind. Common values: `INSTAGRAM`, `TIKTOK`, `YOUTUBE`, `TWITTER`, `LINKEDIN`, `FACEBOOK`, `THREADS`, `EMAIL`, `WEB`. The full list is auto-generated; reject with 400 if unknown."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"Destination for the social link. For most platforms a username (e.g. `youngfrikanna`) or a full URL both work — the renderer normalizes them. For `EMAIL` and `PHONE`, just the address/number."},"label":{"description":"Optional display label override. Most callers can omit this.","anyOf":[{"type":"string","maxLength":60},{"type":"null"}]}},"required":["type","url"],"additionalProperties":false}}},"required":["socials"],"additionalProperties":false},"BlockCreateRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","oneOf":[{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Visible label on the block. Pass null (or omit on create) for image-only blocks where the title is baked into the thumbnail image — fine on `thumbnail_highlight` and `carousel`. `button` without a title renders as an empty rectangle (always set one). `image_background` USES the title to anchor the block height — without a title the block collapses, so always set one for that layout."},"url":{"type":"string","format":"uri","description":"Destination URL the block opens when tapped"},"layout":{"default":"button","type":"string","enum":["button","thumbnail","thumbnail_highlight","thumbnail_grid","image_background","carousel"],"description":"Visual style. All layouts except `button` require a `thumbnailUrl`.\n\n- `button`: standard tappable rectangle, title (and optional caption) only — no image. The default.\n- `thumbnail`: short row with a square thumbnail on the left and title/caption on the right. Best with `textAlign: \"left\"`.\n- `thumbnail_highlight`: TALL, image-dominant card. The thumbnail fills most of the block, with the title rendered below it. Use this for album art, video posters, hero links — anywhere the image is the main attraction.\n- `thumbnail_grid`: square tile arranged into a grid. Set `gridSize: 2` or `3` for column count; consecutive blocks with the same `gridSize` auto-arrange together. Title is small/optional.\n- `image_background`: SHORT banner where the title text is overlaid on top of the thumbnail image. The block height is anchored to the title text size, NOT the image — without a title, it can collapse. Use for compact promo banners, not for showcasing artwork (use `thumbnail_highlight` for that).\n- `carousel`: rotates through images on a swipeable carousel. Consecutive blocks with `layout: \"carousel\"` auto-group into a single carousel."},"caption":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"description":"Secondary text shown beneath the title on supported layouts. Pass null to clear."},"thumbnailUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the thumbnail. Required for the `thumbnail`, `thumbnail_highlight`, `thumbnail_grid`, `image_background`, and `carousel` layouts; ignored on `button`. The server fetches and re-hosts the image; the response returns the rehosted CDN URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF (SVG/ICO/BMP are rejected)."},"labelText":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"description":"Optional short badge shown on the block (e.g. `NEW`, `SALE`, `FEATURED`). Renders with the colors set on profile styles (`labelColor` / `labelTextColor`). Up to 20 characters. Pass null to clear."},"gridSize":{"anyOf":[{"anyOf":[{"type":"number","const":2},{"type":"number","const":3}]},{"type":"null"}],"description":"Number of grid columns. Required when `layout` is `thumbnail_grid` — the value (2 or 3) sets the column count, and consecutive blocks with the same gridSize auto-arrange into a grid. Ignored on other layouts. Pass null (or omit) for a normal full-width block."},"textAlign":{"default":"center","type":"string","enum":["left","center","right"],"description":"Horizontal alignment of the title and caption text within the block. Defaults to `center`. Use `left` for content-heavy blocks (especially `thumbnail` layouts) where the title and caption read naturally as a left-aligned column."},"hidden":{"default":false,"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"default":"home","type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"folderId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When set, nests this block inside the folder with that id. Pass null (or omit) for a top-level block. Create the folder block first, then create children with `folderId` referring to it."},"emoji":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}],"description":"Emoji rendered as a small leading icon on the block (e.g. \"🔥\", \"🎧\"). Only use on `button` layouts: on thumbnail/image layouts the emoji takes precedence over `thumbnailUrl` and renders in the image slot instead of the thumbnail. Pass null to clear."},"type":{"type":"string","const":"link","description":"Block type discriminator. Must be `\"link\"` for a tappable link block."}},"required":["url","layout","textAlign","hidden","pageId","type"],"additionalProperties":false},{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":5000,"description":"Block content as CommonMark Markdown. Supported: paragraphs, **bold**, *italic*, [links](https://...), `inline code`, headings (`#`–`######`), bullet/numbered lists, and blockquotes. Inline HTML in the source is stripped; link/image URLs are limited to `http`, `https`, `mailto`, `tel`, anchors (`#...`), and same-origin paths (`/...`). On read, the field returns the rendered HTML stored on the profile (dashboard-edited blocks may include extra formatting like center-alignment or underline that Markdown can't express)."},"hidden":{"default":false,"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"default":"home","type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"type":{"type":"string","const":"text","description":"Block type discriminator. Must be `\"text\"` for an inline rich-text block."}},"required":["body","hidden","pageId","type"],"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Section heading text shown between blocks (e.g. \"Explore Our Work\"). Pass null for an unlabeled spacer."},"hidden":{"default":false,"type":"boolean","description":"When true, the divider is saved but not shown on the public profile."},"pageId":{"default":"home","type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page)."},"type":{"type":"string","const":"divider","description":"Block type discriminator. Must be `\"divider\"` for a section heading / spacer."}},"required":["hidden","pageId","type"],"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Folder header text (e.g. \"Current Openings\"). Required for `collapsible` style; optional for `inline`."},"style":{"default":"collapsible","type":"string","enum":["collapsible","inline"],"description":"`collapsible` (default) renders as a tappable header that expands to show child blocks — use when you want the children hidden until clicked. `inline` renders the children directly without a folder header — useful for grouping consecutive blocks that should reorder or move together."},"hidden":{"default":false,"type":"boolean","description":"When true, the folder and all its children are hidden from the public profile."},"pageId":{"default":"home","type":"string","minLength":1,"description":"Identifier of the page the folder belongs to. Defaults to `home`."},"type":{"type":"string","const":"folder","description":"Block type discriminator. Must be `\"folder\"`. Create the folder first, then create child link/text blocks with `folderId` set to this block's id."}},"required":["style","hidden","pageId","type"],"additionalProperties":false},{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Public URL of the media to embed (YouTube, Vimeo, Spotify, SoundCloud, Twitch, etc. — anything Iframely supports). The server fetches the embed HTML at create time and re-fetches it whenever this field changes on PATCH."},"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"null"}],"description":"Display title shown in the dashboard for searching/organizing this block. Auto-populated from the media metadata on create (e.g., \"YouTube — Video Title\"); pass a string to override or null to clear. The public profile renders the embed itself, so this title is internal-only."},"embedOptions":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Optional Iframely embed options that customize the rendered iframe (e.g. `{ autoplay: 1, maxwidth: 640 }`). Changing this triggers a re-fetch of the embed HTML."},"hidden":{"default":false,"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"default":"home","type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home`."},"type":{"type":"string","const":"media","description":"Block type discriminator. Must be `\"media\"` for an embedded video/audio/etc. block."}},"required":["url","hidden","pageId","type"],"additionalProperties":false}],"discriminator":{"propertyName":"type"},"type":"object"},"BlockUpdateRequest":{"type":"object","$schema":"https://json-schema.org/draft/2020-12/schema","anyOf":[{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Visible label on the block. Pass null (or omit on create) for image-only blocks where the title is baked into the thumbnail image — fine on `thumbnail_highlight` and `carousel`. `button` without a title renders as an empty rectangle (always set one). `image_background` USES the title to anchor the block height — without a title the block collapses, so always set one for that layout."},"url":{"type":"string","format":"uri","description":"Destination URL the block opens when tapped"},"layout":{"type":"string","enum":["button","thumbnail","thumbnail_highlight","thumbnail_grid","image_background","carousel"],"description":"Visual style. All layouts except `button` require a `thumbnailUrl`.\n\n- `button`: standard tappable rectangle, title (and optional caption) only — no image. The default.\n- `thumbnail`: short row with a square thumbnail on the left and title/caption on the right. Best with `textAlign: \"left\"`.\n- `thumbnail_highlight`: TALL, image-dominant card. The thumbnail fills most of the block, with the title rendered below it. Use this for album art, video posters, hero links — anywhere the image is the main attraction.\n- `thumbnail_grid`: square tile arranged into a grid. Set `gridSize: 2` or `3` for column count; consecutive blocks with the same `gridSize` auto-arrange together. Title is small/optional.\n- `image_background`: SHORT banner where the title text is overlaid on top of the thumbnail image. The block height is anchored to the title text size, NOT the image — without a title, it can collapse. Use for compact promo banners, not for showcasing artwork (use `thumbnail_highlight` for that).\n- `carousel`: rotates through images on a swipeable carousel. Consecutive blocks with `layout: \"carousel\"` auto-group into a single carousel."},"caption":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"description":"Secondary text shown beneath the title on supported layouts. Pass null to clear."},"thumbnailUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the thumbnail. Required for the `thumbnail`, `thumbnail_highlight`, `thumbnail_grid`, `image_background`, and `carousel` layouts; ignored on `button`. The server fetches and re-hosts the image; the response returns the rehosted CDN URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF (SVG/ICO/BMP are rejected)."},"labelText":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"description":"Optional short badge shown on the block (e.g. `NEW`, `SALE`, `FEATURED`). Renders with the colors set on profile styles (`labelColor` / `labelTextColor`). Up to 20 characters. Pass null to clear."},"gridSize":{"anyOf":[{"anyOf":[{"type":"number","const":2},{"type":"number","const":3}]},{"type":"null"}],"description":"Number of grid columns. Required when `layout` is `thumbnail_grid` — the value (2 or 3) sets the column count, and consecutive blocks with the same gridSize auto-arrange into a grid. Ignored on other layouts. Pass null (or omit) for a normal full-width block."},"textAlign":{"type":"string","enum":["left","center","right"],"description":"Horizontal alignment of the title and caption text within the block. Defaults to `center`. Use `left` for content-heavy blocks (especially `thumbnail` layouts) where the title and caption read naturally as a left-aligned column."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"folderId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When set, nests this block inside the folder with that id. Pass null (or omit) for a top-level block. Create the folder block first, then create children with `folderId` referring to it."},"emoji":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}],"description":"Emoji rendered as a small leading icon on the block (e.g. \"🔥\", \"🎧\"). Only use on `button` layouts: on thumbnail/image layouts the emoji takes precedence over `thumbnailUrl` and renders in the image slot instead of the thumbnail. Pass null to clear."},"type":{"type":"string","const":"link"}},"additionalProperties":false},{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":5000,"description":"Block content as CommonMark Markdown. Supported: paragraphs, **bold**, *italic*, [links](https://...), `inline code`, headings (`#`–`######`), bullet/numbered lists, and blockquotes. Inline HTML in the source is stripped; link/image URLs are limited to `http`, `https`, `mailto`, `tel`, anchors (`#...`), and same-origin paths (`/...`). On read, the field returns the rendered HTML stored on the profile (dashboard-edited blocks may include extra formatting like center-alignment or underline that Markdown can't express)."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"type":{"type":"string","const":"text"}},"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Section heading text shown between blocks (e.g. \"Explore Our Work\"). Pass null for an unlabeled spacer."},"hidden":{"type":"boolean","description":"When true, the divider is saved but not shown on the public profile."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page)."},"type":{"type":"string","const":"divider"}},"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Folder header text (e.g. \"Current Openings\"). Required for `collapsible` style; optional for `inline`."},"style":{"type":"string","enum":["collapsible","inline"],"description":"`collapsible` (default) renders as a tappable header that expands to show child blocks — use when you want the children hidden until clicked. `inline` renders the children directly without a folder header — useful for grouping consecutive blocks that should reorder or move together."},"hidden":{"type":"boolean","description":"When true, the folder and all its children are hidden from the public profile."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page the folder belongs to. Defaults to `home`."},"type":{"type":"string","const":"folder"}},"additionalProperties":false},{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Public URL of the media to embed (YouTube, Vimeo, Spotify, SoundCloud, Twitch, etc. — anything Iframely supports). The server fetches the embed HTML at create time and re-fetches it whenever this field changes on PATCH."},"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"null"}],"description":"Display title shown in the dashboard for searching/organizing this block. Auto-populated from the media metadata on create (e.g., \"YouTube — Video Title\"); pass a string to override or null to clear. The public profile renders the embed itself, so this title is internal-only."},"embedOptions":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Optional Iframely embed options that customize the rendered iframe (e.g. `{ autoplay: 1, maxwidth: 640 }`). Changing this triggers a re-fetch of the embed HTML."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home`."},"type":{"type":"string","const":"media"}},"additionalProperties":false}]},"BlockResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"block":{"oneOf":[{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Visible label on the block. Pass null (or omit on create) for image-only blocks where the title is baked into the thumbnail image — fine on `thumbnail_highlight` and `carousel`. `button` without a title renders as an empty rectangle (always set one). `image_background` USES the title to anchor the block height — without a title the block collapses, so always set one for that layout."},"url":{"type":"string","format":"uri","description":"Destination URL the block opens when tapped"},"layout":{"type":"string","enum":["button","thumbnail","thumbnail_highlight","thumbnail_grid","image_background","carousel"],"description":"Visual style. All layouts except `button` require a `thumbnailUrl`.\n\n- `button`: standard tappable rectangle, title (and optional caption) only — no image. The default.\n- `thumbnail`: short row with a square thumbnail on the left and title/caption on the right. Best with `textAlign: \"left\"`.\n- `thumbnail_highlight`: TALL, image-dominant card. The thumbnail fills most of the block, with the title rendered below it. Use this for album art, video posters, hero links — anywhere the image is the main attraction.\n- `thumbnail_grid`: square tile arranged into a grid. Set `gridSize: 2` or `3` for column count; consecutive blocks with the same `gridSize` auto-arrange together. Title is small/optional.\n- `image_background`: SHORT banner where the title text is overlaid on top of the thumbnail image. The block height is anchored to the title text size, NOT the image — without a title, it can collapse. Use for compact promo banners, not for showcasing artwork (use `thumbnail_highlight` for that).\n- `carousel`: rotates through images on a swipeable carousel. Consecutive blocks with `layout: \"carousel\"` auto-group into a single carousel."},"caption":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"description":"Secondary text shown beneath the title on supported layouts. Pass null to clear."},"thumbnailUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the thumbnail. Required for the `thumbnail`, `thumbnail_highlight`, `thumbnail_grid`, `image_background`, and `carousel` layouts; ignored on `button`. The server fetches and re-hosts the image; the response returns the rehosted CDN URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF (SVG/ICO/BMP are rejected)."},"labelText":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"description":"Optional short badge shown on the block (e.g. `NEW`, `SALE`, `FEATURED`). Renders with the colors set on profile styles (`labelColor` / `labelTextColor`). Up to 20 characters. Pass null to clear."},"gridSize":{"anyOf":[{"anyOf":[{"type":"number","const":2},{"type":"number","const":3}]},{"type":"null"}],"description":"Number of grid columns. Required when `layout` is `thumbnail_grid` — the value (2 or 3) sets the column count, and consecutive blocks with the same gridSize auto-arrange into a grid. Ignored on other layouts. Pass null (or omit) for a normal full-width block."},"textAlign":{"type":"string","enum":["left","center","right"],"description":"Horizontal alignment of the title and caption text within the block. Defaults to `center`. Use `left` for content-heavy blocks (especially `thumbnail` layouts) where the title and caption read naturally as a left-aligned column."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"folderId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When set, nests this block inside the folder with that id. Pass null (or omit) for a top-level block. Create the folder block first, then create children with `folderId` referring to it."},"emoji":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}],"description":"Emoji rendered as a small leading icon on the block (e.g. \"🔥\", \"🎧\"). Only use on `button` layouts: on thumbnail/image layouts the emoji takes precedence over `thumbnailUrl` and renders in the image slot instead of the thumbnail. Pass null to clear."},"type":{"type":"string","const":"link"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["title","url","layout","caption","thumbnailUrl","labelText","gridSize","textAlign","hidden","pageId","folderId","emoji","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":5000,"description":"Block content as CommonMark Markdown. Supported: paragraphs, **bold**, *italic*, [links](https://...), `inline code`, headings (`#`–`######`), bullet/numbered lists, and blockquotes. Inline HTML in the source is stripped; link/image URLs are limited to `http`, `https`, `mailto`, `tel`, anchors (`#...`), and same-origin paths (`/...`). On read, the field returns the rendered HTML stored on the profile (dashboard-edited blocks may include extra formatting like center-alignment or underline that Markdown can't express)."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"type":{"type":"string","const":"text"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["body","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Section heading text shown between blocks (e.g. \"Explore Our Work\"). Pass null for an unlabeled spacer."},"hidden":{"type":"boolean","description":"When true, the divider is saved but not shown on the public profile."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page)."},"type":{"type":"string","const":"divider"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["title","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Folder header text (e.g. \"Current Openings\"). Required for `collapsible` style; optional for `inline`."},"style":{"type":"string","enum":["collapsible","inline"],"description":"`collapsible` (default) renders as a tappable header that expands to show child blocks — use when you want the children hidden until clicked. `inline` renders the children directly without a folder header — useful for grouping consecutive blocks that should reorder or move together."},"hidden":{"type":"boolean","description":"When true, the folder and all its children are hidden from the public profile."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page the folder belongs to. Defaults to `home`."},"type":{"type":"string","const":"folder"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["title","style","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Public URL of the media to embed (YouTube, Vimeo, Spotify, SoundCloud, Twitch, etc. — anything Iframely supports). The server fetches the embed HTML at create time and re-fetches it whenever this field changes on PATCH."},"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"null"}],"description":"Display title shown in the dashboard for searching/organizing this block. Auto-populated from the media metadata on create (e.g., \"YouTube — Video Title\"); pass a string to override or null to clear. The public profile renders the embed itself, so this title is internal-only."},"embedOptions":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Optional Iframely embed options that customize the rendered iframe (e.g. `{ autoplay: 1, maxwidth: 640 }`). Changing this triggers a re-fetch of the embed HTML."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home`."},"type":{"type":"string","const":"media"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["url","title","embedOptions","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false}]}},"required":["block"],"additionalProperties":false},"BlocksListResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"blocks":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Visible label on the block. Pass null (or omit on create) for image-only blocks where the title is baked into the thumbnail image — fine on `thumbnail_highlight` and `carousel`. `button` without a title renders as an empty rectangle (always set one). `image_background` USES the title to anchor the block height — without a title the block collapses, so always set one for that layout."},"url":{"type":"string","format":"uri","description":"Destination URL the block opens when tapped"},"layout":{"type":"string","enum":["button","thumbnail","thumbnail_highlight","thumbnail_grid","image_background","carousel"],"description":"Visual style. All layouts except `button` require a `thumbnailUrl`.\n\n- `button`: standard tappable rectangle, title (and optional caption) only — no image. The default.\n- `thumbnail`: short row with a square thumbnail on the left and title/caption on the right. Best with `textAlign: \"left\"`.\n- `thumbnail_highlight`: TALL, image-dominant card. The thumbnail fills most of the block, with the title rendered below it. Use this for album art, video posters, hero links — anywhere the image is the main attraction.\n- `thumbnail_grid`: square tile arranged into a grid. Set `gridSize: 2` or `3` for column count; consecutive blocks with the same `gridSize` auto-arrange together. Title is small/optional.\n- `image_background`: SHORT banner where the title text is overlaid on top of the thumbnail image. The block height is anchored to the title text size, NOT the image — without a title, it can collapse. Use for compact promo banners, not for showcasing artwork (use `thumbnail_highlight` for that).\n- `carousel`: rotates through images on a swipeable carousel. Consecutive blocks with `layout: \"carousel\"` auto-group into a single carousel."},"caption":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"description":"Secondary text shown beneath the title on supported layouts. Pass null to clear."},"thumbnailUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Public URL of an image to use as the thumbnail. Required for the `thumbnail`, `thumbnail_highlight`, `thumbnail_grid`, `image_background`, and `carousel` layouts; ignored on `button`. The server fetches and re-hosts the image; the response returns the rehosted CDN URL. Pass null to clear. Supported formats: PNG, JPEG, WEBP, GIF (SVG/ICO/BMP are rejected)."},"labelText":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"description":"Optional short badge shown on the block (e.g. `NEW`, `SALE`, `FEATURED`). Renders with the colors set on profile styles (`labelColor` / `labelTextColor`). Up to 20 characters. Pass null to clear."},"gridSize":{"anyOf":[{"anyOf":[{"type":"number","const":2},{"type":"number","const":3}]},{"type":"null"}],"description":"Number of grid columns. Required when `layout` is `thumbnail_grid` — the value (2 or 3) sets the column count, and consecutive blocks with the same gridSize auto-arrange into a grid. Ignored on other layouts. Pass null (or omit) for a normal full-width block."},"textAlign":{"type":"string","enum":["left","center","right"],"description":"Horizontal alignment of the title and caption text within the block. Defaults to `center`. Use `left` for content-heavy blocks (especially `thumbnail` layouts) where the title and caption read naturally as a left-aligned column."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"folderId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"When set, nests this block inside the folder with that id. Pass null (or omit) for a top-level block. Create the folder block first, then create children with `folderId` referring to it."},"emoji":{"anyOf":[{"type":"string","minLength":1,"maxLength":64},{"type":"null"}],"description":"Emoji rendered as a small leading icon on the block (e.g. \"🔥\", \"🎧\"). Only use on `button` layouts: on thumbnail/image layouts the emoji takes precedence over `thumbnailUrl` and renders in the image slot instead of the thumbnail. Pass null to clear."},"type":{"type":"string","const":"link"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["title","url","layout","caption","thumbnailUrl","labelText","gridSize","textAlign","hidden","pageId","folderId","emoji","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":5000,"description":"Block content as CommonMark Markdown. Supported: paragraphs, **bold**, *italic*, [links](https://...), `inline code`, headings (`#`–`######`), bullet/numbered lists, and blockquotes. Inline HTML in the source is stripped; link/image URLs are limited to `http`, `https`, `mailto`, `tel`, anchors (`#...`), and same-origin paths (`/...`). On read, the field returns the rendered HTML stored on the profile (dashboard-edited blocks may include extra formatting like center-alignment or underline that Markdown can't express)."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page). Multi-page profiles can target other page IDs."},"type":{"type":"string","const":"text"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["body","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Section heading text shown between blocks (e.g. \"Explore Our Work\"). Pass null for an unlabeled spacer."},"hidden":{"type":"boolean","description":"When true, the divider is saved but not shown on the public profile."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home` (the main page)."},"type":{"type":"string","const":"divider"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["title","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":200},{"type":"null"}],"description":"Folder header text (e.g. \"Current Openings\"). Required for `collapsible` style; optional for `inline`."},"style":{"type":"string","enum":["collapsible","inline"],"description":"`collapsible` (default) renders as a tappable header that expands to show child blocks — use when you want the children hidden until clicked. `inline` renders the children directly without a folder header — useful for grouping consecutive blocks that should reorder or move together."},"hidden":{"type":"boolean","description":"When true, the folder and all its children are hidden from the public profile."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page the folder belongs to. Defaults to `home`."},"type":{"type":"string","const":"folder"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["title","style","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false},{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Public URL of the media to embed (YouTube, Vimeo, Spotify, SoundCloud, Twitch, etc. — anything Iframely supports). The server fetches the embed HTML at create time and re-fetches it whenever this field changes on PATCH."},"title":{"anyOf":[{"type":"string","minLength":1,"maxLength":500},{"type":"null"}],"description":"Display title shown in the dashboard for searching/organizing this block. Auto-populated from the media metadata on create (e.g., \"YouTube — Video Title\"); pass a string to override or null to clear. The public profile renders the embed itself, so this title is internal-only."},"embedOptions":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"}]},"description":"Optional Iframely embed options that customize the rendered iframe (e.g. `{ autoplay: 1, maxwidth: 640 }`). Changing this triggers a re-fetch of the embed HTML."},"hidden":{"type":"boolean","description":"When true, the block is saved but not shown on the public profile. Defaults to false."},"pageId":{"type":"string","minLength":1,"description":"Identifier of the page this block belongs to. Defaults to `home`."},"type":{"type":"string","const":"media"},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"updatedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"}},"required":["url","title","embedOptions","hidden","pageId","type","id","createdAt","updatedAt"],"additionalProperties":false}]}}},"required":["blocks"],"additionalProperties":false},"ReorderBlocksRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"pageId":{"default":"home","description":"Identifier of the page to reorder. Defaults to `home`. Must match the page IDs returned by `GET /profiles/{profileId}/blocks`.","type":"string","minLength":1},"orderedIds":{"minItems":1,"type":"array","items":{"type":"string","minLength":1},"description":"Block IDs in their new top-to-bottom order. Must include EXACTLY the IDs currently on the page — no missing IDs and no extras. Use `GET /profiles/{profileId}/blocks?pageId=...` to fetch the current set first."}},"required":["pageId","orderedIds"],"additionalProperties":false},"PagesListResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"pages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Page identifier. The home page always has the literal id `home`; custom pages have cuid-style ids. Pass this value to `?pageId=` on `GET /profiles/{profileId}/blocks` or to `pageId` in `POST /profiles/{profileId}/blocks/reorder`."},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Display title for the page (shown in the in-app page picker). `null` for the home page and for untitled custom pages."},"urlSlug":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Path segment appended to the profile URL — custom pages render at `https://liinks.co/<profile-slug>/<urlSlug>`. `null` for the home page (which lives at the profile root) and for custom pages without a slug assigned yet."},"isHome":{"type":"boolean","description":"True for the implicit home page (always the first entry in the list), false otherwise. Exactly one entry per profile has `isHome: true`."}},"required":["id","title","urlSlug","isHome"],"additionalProperties":false}}},"required":["pages"],"additionalProperties":false},"FormSubmissionsListResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"submissions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier for this submission. Use this for deduplication when polling — combined with `createdAt`-based `?since=` filtering, it lets callers cleanly resume after their last seen response."},"formId":{"type":"string","description":"ID of the form this submission belongs to. Multiple submissions can share a `formId` (a form receives many responses); group by this field to per-form filter without follow-up calls."},"formTitle":{"type":"string","description":"Title of the form, joined into the response for convenience so consumers can filter or label without a follow-up call. Empty string if the form was deleted between submission and read (unusual — submissions cascade-delete with their form)."},"subscriberId":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ID of the matching `EmailSub` row when the form had `subscriberMode` on AND the submission included a valid `EMAIL` answer. `null` otherwise. Use `GET /profiles/{profileId}/subscribers` to read the subscriber record."},"answers":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"Question label as configured on the form. Stable across submissions of the same form — safe to use as a key when mapping answers to downstream fields."},"value":{"type":"string","description":"Visitor-submitted answer. Empty string if the field was optional and left blank. For `MULTI_SELECT` questions, the selected options are joined by commas."},"questionType":{"type":"string","enum":["SHORT_TEXT","LONG_TEXT","DROPDOWN","EMAIL","PHONE","URL","NUMBER","CHECKBOX","RADIO","DATE","TIME","DATETIME","RATING","PASSWORD","ADDRESS","NAME"],"description":"Question kind, defining how `value` should be interpreted. Common values: `SHORT_TEXT`, `LONG_TEXT`, `EMAIL`, `PHONE`, `MULTI_SELECT`, `DROPDOWN`, `NUMBER`, `DATE`, `CHECKBOX`. `PASSWORD` answers are never returned — they are validated server-side at submit time and discarded."}},"required":["label","value","questionType"],"additionalProperties":false},"description":"Submitted answers in the order the questions appear on the form. PASSWORD answers are stripped before storage and never appear here."},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When the visitor submitted the form. Submissions are returned newest-first; use the highest `createdAt` seen as `?since=<iso>` on the next call to receive only new submissions."}},"required":["id","formId","formTitle","subscriberId","answers","createdAt"],"additionalProperties":false}}},"required":["submissions"],"additionalProperties":false},"SubscribersListResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"subscribers":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier for the subscriber. Use this for deduplication when polling — combined with `createdAt`-based `?since=` filtering, it lets callers cleanly resume after their last seen subscriber."},"email":{"type":"string","description":"Subscriber email address as submitted by the visitor. Not bounce-validated: emails are accepted on a permissive format check, so syncing downstream should expect occasional invalid addresses."},"createdAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$","description":"When the subscriber was first added. Subscribers are returned newest-first; use the highest `createdAt` seen as `?since=<iso>` on the next call to receive only new subscribers. Adding the same email twice on the same profile reuses the original row — `createdAt` reflects the first sign-up."}},"required":["id","email","createdAt"],"additionalProperties":false}}},"required":["subscribers"],"additionalProperties":false},"ScreenshotRequest":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"viewportWidth":{"description":"Viewport width in CSS pixels. Defaults to 375 (mobile, matching the profile's design width). Use a desktop width like 1280 to capture the desktop layout.","type":"integer","minimum":320,"maximum":1920},"viewportHeight":{"description":"Viewport height in CSS pixels. Defaults to 812. Each returned page covers exactly this many vertical pixels of the profile.","type":"integer","minimum":480,"maximum":1920},"maxPages":{"description":"Cap on the number of paginated images returned. Defaults to 10; hard maximum is 20. If the rendered page is taller than `maxPages * viewportHeight`, the response sets `truncated: true` and the tail is dropped.","type":"integer","minimum":1,"maximum":20}},"additionalProperties":false},"ScreenshotResponse":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"viewport":{"type":"object","properties":{"width":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"height":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["width","height"],"additionalProperties":false,"description":"The viewport size used for the capture."},"totalHeight":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total scrollable height of the rendered profile in CSS pixels. Use `Math.ceil(totalHeight / viewport.height)` to know how many pages a non-truncated capture would produce."},"truncated":{"type":"boolean","description":"True when `Math.ceil(totalHeight / viewport.height) > maxPages` and the bottom of the profile was not captured. Re-call with a larger `maxPages` (up to the documented hard cap) to get the rest."},"pages":{"type":"array","items":{"type":"object","properties":{"pageNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"1-indexed position of this image in the top-to-bottom sequence."},"scrollY":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Vertical scroll offset (in CSS pixels) at which this image was captured. Page N covers pixels `scrollY` through `scrollY + viewportHeight`."},"url":{"type":"string","format":"uri","description":"Public CDN URL of the JPEG. URLs are stable but the underlying file is not refreshed — call this endpoint again to capture new changes."}},"required":["pageNumber","scrollY","url"],"additionalProperties":false},"description":"Captured pages in top-to-bottom order. Always at least one entry, even for short profiles."}},"required":["viewport","totalHeight","truncated","pages"],"additionalProperties":false}}},"paths":{"/me":{"get":{"operationId":"getMe","summary":"Get the account that owns this API key","security":[{"bearerAuth":[]}],"description":"Returns identifying info for the user behind the bearer token: id, slug, email, displayName, and current subscription plan. The canonical way to surface a \"connected as …\" label or confirm which account an API key belongs to after a user pastes it. Cheap to call (no DB joins beyond the auth lookup) — use it on first connection and to refresh the cached label periodically. The returned `id` matches the user's own entry in `GET /profiles`.","tags":["Account"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"},"example":{"user":{"id":"cmoab1234567890abcdefghij","slug":"jane.doe","email":"jane@example.com","displayName":"Jane Doe","plan":"PREMIUM_2"}}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles":{"get":{"operationId":"listProfiles","summary":"List profiles owned by this API key","security":[{"bearerAuth":[]}],"description":"Returns the API key owner's profile plus any child profiles they manage. Always includes at least one entry (the owner). Start every workflow here so you have valid profile `id`s for the routes below.","tags":["Profiles"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilesListResponse"},"example":{"profiles":[{"id":"cmoab1234567890abcdefghij","slug":"jane.doe","displayName":"Jane Doe","bio":"Photographer based in NYC","tagline":null,"profilePictureUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/abc123def456.png","socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"EMAIL","url":"jane@example.com"}],"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}]}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createProfile","summary":"Create a new managed profile","security":[{"bearerAuth":[]}],"description":"Provisions a new child profile under the API key owner. Requires a plan that supports managing multiple profiles — returns 402 otherwise. The plan's profile limit also applies: once the account is at its cap, further creates return 402 with `error.details.limit` and `error.details.current`. The slug must be unique across all of Liinks; on collision the response is 409.","tags":["Profiles"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileCreateRequest"},"example":{"slug":"jane.doe","displayName":"Jane Doe","bio":"Photographer based in NYC","profilePictureUrl":"https://example.com/avatar.jpg","socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"EMAIL","url":"jane@example.com"}]}}}},"responses":{"201":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileCreateResponse"},"example":{"profile":{"id":"cmoab1234567890abcdefghij","slug":"jane.doe","displayName":"Jane Doe","bio":"Photographer based in NYC","tagline":null,"profilePictureUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/abc123def456.png","socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"EMAIL","url":"jane@example.com"}],"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The account does not have a plan that supports the requested capability (see `error.details.requiredCapability`). Not retry-able — either upgrade the underlying account or use an API key whose owner has the right plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Slug is already taken or fails format rules. Pick a different slug and retry — see `error.message` for the specific reason.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"getProfile","summary":"Get a single profile","security":[{"bearerAuth":[]}],"description":"Fetches a single profile by id. Use this to read the current state before computing a `PATCH` body, or to confirm a write took effect. The response shape matches what `PATCH` accepts (minus server-managed fields like `id`/`createdAt`/`updatedAt`).","tags":["Profiles"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"},"example":{"profile":{"id":"cmoab1234567890abcdefghij","slug":"jane.doe","displayName":"Jane Doe","bio":"Photographer based in NYC","tagline":null,"profilePictureUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/abc123def456.png","socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"EMAIL","url":"jane@example.com"}],"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateProfile","summary":"Update profile fields","security":[{"bearerAuth":[]}],"description":"Partial update — send only the fields you want to change. Pass `null` for `displayName`, `bio`, or `tagline` to clear them. Slug changes are immediate and break any external links to the old slug.","tags":["Profiles"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileUpdateRequest"},"example":{"displayName":"Jane Doe","bio":"Photographer based in NYC"}}}},"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileResponse"},"example":{"profile":{"id":"cmoab1234567890abcdefghij","slug":"jane.doe","displayName":"Jane Doe","bio":"Photographer based in NYC","tagline":null,"profilePictureUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/abc123def456.png","socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"EMAIL","url":"jane@example.com"}],"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The account does not have a plan that supports the requested capability (see `error.details.requiredCapability`). Not retry-able — either upgrade the underlying account or use an API key whose owner has the right plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Slug change collides with an existing user or fails format rules. Pick a different slug and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteProfile","summary":"Delete a managed profile","security":[{"bearerAuth":[]}],"description":"Hard-delete a managed child profile. The API key owner's own profile cannot be deleted via the API (returns 400) — use the dashboard for self-deletion. There is no undo: the user record, all blocks, forms, form responses, and analytics are permanently removed. The Stripe seat count is updated automatically.","tags":["Profiles"],"responses":{"204":{"description":"No content","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/styles":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"getStyles","summary":"Get profile styles","security":[{"bearerAuth":[]}],"description":"Returns the full theme: colors, fonts, background, header layout, and ratio knobs. Use the result as a template for PATCH bodies — copy what you want to keep and replace what you want to change.","tags":["Styles"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StylesResponse"},"example":{"styles":{"buttonColor":"#f5f5f5","buttonTextColor":"#1a1a1a","labelColor":"#1b97f5","labelTextColor":"#ffffff","ctaColor":"#1b97f5","ctaTextColor":"#ffffff","foregroundColor":"#1a1a1a","sheetColor":"#fafafa","headerTextColor":"#1a1a1a","borderColor":"#e0e0e0","headerLayout":"HEADSHOT","fontStyle1":{"family":"Inter","style":"700"},"fontStyle2":{"family":"Inter","style":"400"},"background":{"backgroundType":"GRADIENT","backgroundValue":"linear-gradient(180deg, #fef3e8 0%, #f5e6f3 100%)"},"desktopBackground":{"backgroundType":"NONE","backgroundValue":null},"backgroundImageUrl":null,"shadowRatio":0.4,"borderRadiusRatio":0.6,"borderWidthRatio":0,"spacingRatio":0.4,"enableSheetFade":true,"enableSearch":false,"tactileLinkType":"FLAT"}}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateStyles","summary":"Update profile styles","security":[{"bearerAuth":[]}],"description":"Partial update. Pass only the keys you want to change. `null` clears a value back to defaults. For `background` / `desktopBackground` / `fontStyle1` / `fontStyle2`, provide the entire nested object — partial updates inside those nested objects are not supported. Heads-up: when `tactileLinkType` is anything other than `NONE` (including a previously-set value), the renderer derives button surfaces from the page color and IGNORES `buttonColor`, `buttonTextColor`, `borderColor`, `shadowRatio`, and `borderWidthRatio` — when setting explicit button colors, send `tactileLinkType: \"NONE\"` in the same request unless you know it is already `NONE`.","tags":["Styles"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StylesUpdateRequest"},"example":{"buttonColor":"#f5f5f5","buttonTextColor":"#1a1a1a","background":{"backgroundType":"GRADIENT","backgroundValue":"linear-gradient(180deg, #fef3e8 0%, #f5e6f3 100%)"},"fontStyle1":{"family":"Inter","style":"700"},"spacingRatio":0.4,"tactileLinkType":"FLAT"}}}},"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StylesResponse"},"example":{"styles":{"buttonColor":"#f5f5f5","buttonTextColor":"#1a1a1a","labelColor":"#1b97f5","labelTextColor":"#ffffff","ctaColor":"#1b97f5","ctaTextColor":"#ffffff","foregroundColor":"#1a1a1a","sheetColor":"#fafafa","headerTextColor":"#1a1a1a","borderColor":"#e0e0e0","headerLayout":"HEADSHOT","fontStyle1":{"family":"Inter","style":"700"},"fontStyle2":{"family":"Inter","style":"400"},"background":{"backgroundType":"GRADIENT","backgroundValue":"linear-gradient(180deg, #fef3e8 0%, #f5e6f3 100%)"},"desktopBackground":{"backgroundType":"NONE","backgroundValue":null},"backgroundImageUrl":null,"shadowRatio":0.4,"borderRadiusRatio":0.6,"borderWidthRatio":0,"spacingRatio":0.4,"enableSheetFade":true,"enableSearch":false,"tactileLinkType":"FLAT"}}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The account does not have a plan that supports the requested capability (see `error.details.requiredCapability`). Not retry-able — either upgrade the underlying account or use an API key whose owner has the right plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/socials":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"getSocials","summary":"Get social links","security":[{"bearerAuth":[]}],"description":"Returns the social-icon row in display order. Use the result as a template for the `PUT` body.","tags":["Profiles"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialsResponse"},"example":{"socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"WEB","url":"https://janedoe.com"}]}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"setSocials","summary":"Set social links","security":[{"bearerAuth":[]}],"description":"Replaces the social-icon row with the provided ordered list — omit an entry to remove it, reorder the array to reorder the row. Per-type ids and click/view counts are preserved across the write, so editing a URL or reordering never resets a platform’s analytics. Usernames are accepted and normalized to full URLs by the renderer.","tags":["Profiles"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialsSetRequest"},"example":{"socials":[{"type":"INSTAGRAM","url":"janedoe"},{"type":"TIKTOK","url":"https://tiktok.com/@janedoe"},{"type":"WEB","url":"https://janedoe.com"}]}}}},"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SocialsResponse"},"example":{"socials":[{"type":"INSTAGRAM","url":"https://instagram.com/janedoe"},{"type":"WEB","url":"https://janedoe.com"}]}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/blocks":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"listBlocks","summary":"List blocks on a profile","security":[{"bearerAuth":[]}],"description":"Blocks are returned in display order (top to bottom). Pass `?pageId=home` to filter to a single page; omit to return every block on the profile. Required reading before any reorder operation — `POST /blocks/reorder` only accepts the exact id set this endpoint returns.","tags":["Blocks"],"parameters":[{"name":"pageId","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to blocks on a specific page (e.g. `home`). Omit to list every block on the profile."}],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlocksListResponse"},"example":{"blocks":[{"type":"link","id":"cmoab9876543210zyxwvutsrqp","title":"My latest album","url":"https://example.com/album","layout":"thumbnail_highlight","caption":"Out now on all platforms","thumbnailUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/cover123.png","labelText":null,"gridSize":null,"textAlign":"center","hidden":false,"pageId":"home","folderId":null,"emoji":null,"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}]}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createBlock","summary":"Create a new block","security":[{"bearerAuth":[]}],"description":"Body shape depends on the `type` discriminator. Supported types: `link`, `text`, `divider`, `folder`, `media`. Other block types the dashboard offers (forms, subscribe blocks, FAQs, etc.) cannot be created via this API yet — even though read endpoints like `/form-submissions` and `/subscribers` exist. By default the new block is inserted at the top of the page; pass `?position=bottom` to append to the end, or `?after=<blockId>` to insert directly beneath an existing block. For larger reorders, use `POST /blocks/reorder`. Folders: create the folder first, then create child blocks with `folderId` set to the folder's id.","tags":["Blocks"],"parameters":[{"name":"position","in":"query","required":false,"schema":{"type":"string","enum":["top","bottom"]},"description":"Where to place the new block on its page. `top` (default) inserts at the top; `bottom` appends to the end. Mutually exclusive with `after`."},{"name":"after","in":"query","required":false,"schema":{"type":"string"},"description":"Block id to insert directly beneath. The new block lands right after this one and inherits its page (and folder, if it lives in one). Mutually exclusive with `position`."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockCreateRequest"},"examples":{"link":{"summary":"Link block (tappable URL)","value":{"type":"link","title":"My latest album","url":"https://example.com/album","layout":"thumbnail_highlight","thumbnailUrl":"https://example.com/cover.jpg","caption":"Out now on all platforms"}},"text":{"summary":"Text block (Markdown)","value":{"type":"text","body":"## About me\n\nI'm a **photographer** based in NYC. See my [latest work](https://example.com)."}},"divider":{"summary":"Divider (section heading or spacer)","value":{"type":"divider","title":"Recent Releases"}},"folder":{"summary":"Folder (create first, then add child blocks via `folderId`)","value":{"type":"folder","title":"Current Openings","style":"collapsible"}},"media":{"summary":"Media embed (YouTube, Vimeo, Spotify, etc.)","value":{"type":"media","url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}}}}}},"responses":{"201":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockResponse"},"example":{"block":{"type":"link","id":"cmoab9876543210zyxwvutsrqp","title":"My latest album","url":"https://example.com/album","layout":"thumbnail_highlight","caption":"Out now on all platforms","thumbnailUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/cover123.png","labelText":null,"gridSize":null,"textAlign":"center","hidden":false,"pageId":"home","folderId":null,"emoji":null,"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/blocks/{blockId}":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."},{"name":"blockId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the block. Returned in the response of any block create/list call."}],"get":{"operationId":"getBlock","summary":"Get a single block","security":[{"bearerAuth":[]}],"description":"Fetches a single block by id. Useful for reading the current state before computing a `PATCH` body. Response shape varies by `type` (same discriminated union as `POST /blocks` returns).","tags":["Blocks"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockResponse"},"example":{"block":{"type":"link","id":"cmoab9876543210zyxwvutsrqp","title":"My latest album","url":"https://example.com/album","layout":"thumbnail_highlight","caption":"Out now on all platforms","thumbnailUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/cover123.png","labelText":null,"gridSize":null,"textAlign":"center","hidden":false,"pageId":"home","folderId":null,"emoji":null,"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"updateBlock","summary":"Update a block","security":[{"bearerAuth":[]}],"description":"Partial update — only the fields you supply are changed. The block `type` cannot be changed; the body shape is the partial form of the matching block type. Common patterns: `{ \"hidden\": true }` to hide, `{ \"hidden\": false }` to show, `{ \"pageId\": \"<id>\" }` to move between pages, `{ \"folderId\": \"<id>\" }` to move into a folder (or `null` to remove from one).","tags":["Blocks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockUpdateRequest"},"examples":{"hide":{"summary":"Hide the block (without deleting it)","value":{"hidden":true}},"show":{"summary":"Show a hidden block","value":{"hidden":false}},"renameLink":{"summary":"Rename a link and change its URL","value":{"title":"New title","url":"https://example.com/new"}},"editText":{"summary":"Replace a text block's Markdown body","value":{"body":"## Updated heading\n\nNew content here."}},"movePage":{"summary":"Move a block to a different page","value":{"pageId":"about"}},"moveIntoFolder":{"summary":"Move a block into a folder (use `null` to remove)","value":{"folderId":"cmoabfolder123"}}}}}},"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockResponse"},"example":{"block":{"type":"link","id":"cmoab9876543210zyxwvutsrqp","title":"New title","url":"https://example.com/album","layout":"thumbnail_highlight","caption":"Updated caption","thumbnailUrl":"https://d1ym67wyom4bkd.cloudfront.net/upload/cover123.png","labelText":null,"gridSize":null,"textAlign":"center","hidden":false,"pageId":"home","folderId":null,"emoji":null,"createdAt":"2026-04-28T18:30:00.000Z","updatedAt":"2026-04-28T18:30:00.000Z"}}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteBlock","summary":"Delete a block","security":[{"bearerAuth":[]}],"description":"Soft delete. The block is removed from the profile immediately and cannot be restored via the API. To temporarily hide a block instead, use `PATCH` with `{ \"hidden\": true }`.","tags":["Blocks"],"responses":{"204":{"description":"No content","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/blocks/reorder":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"post":{"operationId":"reorderBlocks","summary":"Reorder blocks on a page","security":[{"bearerAuth":[]}],"description":"Sets the top-to-bottom order of blocks on a single page. `orderedIds` MUST include exactly the block IDs currently on the page — no missing IDs and no extras. To get the current set, call `GET /profiles/{profileId}/blocks?pageId=<id>` first. For single-block insertions, prefer `POST /blocks` with `?position=` or `?after=` instead of a full reorder.","tags":["Blocks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderBlocksRequest"},"example":{"pageId":"home","orderedIds":["cmoab123...","cmoab456...","cmoab789..."]}}}},"responses":{"204":{"description":"No content","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/pages":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"listPages","summary":"List pages on a profile","security":[{"bearerAuth":[]}],"description":"Returns every page on the profile in display order, with the implicit `home` page always first (it lives at the profile root and is never stored as a row). Use the returned `id` values wherever an endpoint accepts a page identifier: `?pageId=` on `GET /profiles/{profileId}/blocks`, the `pageId` field in `POST /profiles/{profileId}/blocks/reorder`, and the `pageId` field on block create/update bodies. Pages are not paginated (small set per profile, typically <10). Single-page profiles still return one entry — the home page — so callers can treat the response uniformly.","tags":["Pages"],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PagesListResponse"},"example":{"pages":[{"id":"home","title":null,"urlSlug":null,"isHome":true},{"id":"cmoabpage1234567890abcd","title":"About","urlSlug":"about","isHome":false}]}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/form-submissions":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"listFormSubmissions","summary":"List form submissions on a profile","security":[{"bearerAuth":[]}],"description":"Returns visitor responses across every form on the profile, newest first. Each entry includes the form title alongside the answers so consumers can filter or group by form without a follow-up `GET /forms/{formId}` call. `PASSWORD` answers are stripped server-side before storage and never appear; all other question types are returned verbatim. Empty arrays (no submissions yet) come back as `{ \"submissions\": [] }`, not a 404. Designed for polling: keep the highest `createdAt` you observe and pass it as `?since=<iso>` on the next call to receive only newer submissions; dedupe by `id` for safety. To back-fill historical submissions, omit `since` and use `limit` (max 200 per call) — note that newer submissions arriving between polls will shift the window, so back-fills are best done in a single pass against a known time bound.","tags":["Form submissions"],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Max results to return per call. Default 50, max 200. Results are always sorted newest-first (`createdAt` desc). The response is the most recent `limit` items matching the filter — there is no `next` cursor in the response. To page through history, pair this with `since`/`createdAt` (older items disappear as new ones arrive, so back-fills should fetch on a stable timestamp window)."},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"ISO 8601 timestamp. Only return items with `createdAt` strictly greater than this value. Designed for polling consumers: after each response, store the highest `createdAt` you observed and pass it on the next request. Dedupe by `id` for safety, since two items can share a millisecond. Returns an empty array (not an error) when nothing is newer."}],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormSubmissionsListResponse"},"example":{"submissions":[{"id":"cmoabresp1234567890abcd","formId":"cmoabform1234567890abcd","formTitle":"Newsletter signup","subscriberId":"cmoabsub1234567890abcdef","answers":[{"label":"Email","value":"fan@example.com","questionType":"EMAIL"},{"label":"Name","value":"Sam","questionType":"SHORT_TEXT"}],"createdAt":"2026-05-10T14:25:00.000Z"}]}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/subscribers":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"get":{"operationId":"listSubscribers","summary":"List email subscribers on a profile","security":[{"bearerAuth":[]}],"description":"Returns email subscribers on the profile, newest first. Subscribers are auto-created when a form with subscriber mode receives a submission containing a valid `EMAIL` answer, and can also be added manually from the dashboard. Resubmitting the same email reuses the original subscriber row, so `createdAt` reflects the first sign-up. Empty arrays (no subscribers yet) come back as `{ \"subscribers\": [] }`, not a 404. Designed for polling: keep the highest `createdAt` you observe and pass it as `?since=<iso>` on the next call; dedupe by `id` for safety. For the source form-submission record (when subscriber mode created the row), filter `GET /form-submissions` by the matching `subscriberId`.","tags":["Subscribers"],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Max results to return per call. Default 50, max 200. Results are always sorted newest-first (`createdAt` desc). The response is the most recent `limit` items matching the filter — there is no `next` cursor in the response. To page through history, pair this with `since`/`createdAt` (older items disappear as new ones arrive, so back-fills should fetch on a stable timestamp window)."},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"ISO 8601 timestamp. Only return items with `createdAt` strictly greater than this value. Designed for polling consumers: after each response, store the highest `createdAt` you observed and pass it on the next request. Dedupe by `id` for safety, since two items can share a millisecond. Returns an empty array (not an error) when nothing is newer."}],"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscribersListResponse"},"example":{"subscribers":[{"id":"cmoabsub1234567890abcdef","email":"fan@example.com","createdAt":"2026-05-10T14:25:00.000Z"}]}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/profiles/{profileId}/screenshot":{"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"},"description":"ID of the target profile. Use `GET /profiles` to list profiles owned by the API key."}],"post":{"operationId":"screenshotProfile","summary":"Screenshot the profile for visual verification","security":[{"bearerAuth":[]}],"description":"Renders `https://liinks.co/<slug>` in headless Chrome and returns one or more JPEGs covering the full scrollable page in viewport-sized chunks. Use this after a `PATCH` / `POST` to confirm the rendered result matches expectations — the response captures what a real visitor sees, not just the underlying data. Pages are returned in top-to-bottom order; if the profile is taller than `maxPages * viewportHeight`, the response sets `truncated: true` and re-call with a higher `maxPages` to get the rest. Each call renders fresh; URLs are not cached.","tags":["Screenshot"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenshotRequest"},"examples":{"defaults":{"summary":"Default mobile capture (375×812)","value":{}},"desktop":{"summary":"Desktop layout (1280×800)","value":{"viewportWidth":1280,"viewportHeight":800}},"longProfile":{"summary":"Long profile — allow more pages","value":{"maxPages":15}}}}}},"responses":{"200":{"description":"Successful response","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenshotResponse"},"example":{"viewport":{"width":375,"height":812},"totalHeight":1820,"truncated":false,"pages":[{"pageNumber":1,"scrollY":0,"url":"https://d1ym67wyom4bkd.cloudfront.net/upload/abc123.jpg"},{"pageNumber":2,"scrollY":812,"url":"https://d1ym67wyom4bkd.cloudfront.net/upload/def456.jpg"},{"pageNumber":3,"scrollY":1624,"url":"https://d1ym67wyom4bkd.cloudfront.net/upload/ghi789.jpg"}]}}}},"400":{"description":"Request body or query failed validation. Inspect `error.details.issues[]` (Zod-shaped: each entry has `path`, `code`, `message`) and resend with the failing fields corrected. Common causes: missing required field, unknown field on a strict schema, wrong type for the `type` discriminator on a block.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed, or revoked Bearer token. Verify the `Authorization: Bearer liinks_live_<token>` header is present and intact. Generate or rotate keys at Settings → API Keys in the dashboard.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The requested profile or block does not exist (or has been deleted). Re-list with `GET /profiles` or `GET /profiles/{profileId}/blocks` and use a current id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Wait `Retry-After` (also `error.details.retryAfterSeconds`) seconds before retrying. Mutating verbs (POST/PATCH/DELETE) share a stricter 30/min bucket on top of the 120/min global bucket — batch updates where possible.","headers":{"RateLimit-Limit":{"description":"Quota of the most constrained bucket for this request (global 120/min; writes also 30/min).","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests remaining in that bucket's rolling 60s window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Upper bound in seconds until the bucket frees capacity.","schema":{"type":"integer"}},"RateLimit-Policy":{"description":"Advertised quota policy (also sent on unauthenticated responses): \"global\";q=120;w=60, \"write\";q=30;w=60","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying (the rolling-window size).","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Unexpected server error. Retry once after a brief delay; if it persists, contact hello@liinks.co with the response timestamp.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}