Short links and trackable QR codes: editable after printing, device routing, scan stats.
Maintain this server? Claim it
Call this server's tools from your browser. Nothing to install.
Bearer esy_... API key. An agent can get one itself: https://esyurl.fyi/auth.md
Requests go through mcptry because browsers can't call most MCP servers directly. Header values are sent to the server for that request only and are never stored or logged.
Tool list from our last check. Connect live to refresh it.
Add up to 25 links to a group in one call. Idempotent: links already in the group are reported in alreadyMembers rather than failing. Fails with not_found if any linkId is unknown (nothing is added).
The group id (groupId), as returned by create_group or list_groups
1-25 linkIds
This tool isn't marked read-only, so it may change something on the server or in an account you connected.
Add up to 25 links to a group in one call. Idempotent: links already in the group are reported in alreadyMembers rather than failing. Fails with not_found if any linkId is unknown (nothing is added).
| Argument | Type | Description |
|---|---|---|
| groupId* | string | The group id (groupId), as returned by create_group or list_groups |
| linkIds* | array | 1-25 linkIds |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groupId",
"linkIds"
],
"properties": {
"groupId": {
"type": "string",
"minLength": 1,
"description": "The group id (groupId), as returned by create_group or list_groups"
},
"linkIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "1-25 linkIds"
}
}
}Create a group to organise short links (e.g. a campaign or a venue). Groups are many-to-many: a link can be in up to 20 groups. Add links with add_links_to_group, or pass groupIds when creating/updating a link.
| Argument | Type | Description |
|---|---|---|
| name* | string | Group name, up to 100 chars |
| color | string | Hex colour for UIs, e.g. #3366ff |
| description | string | Up to 500 chars |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"description": "Group name, up to 100 chars"
},
"color": {
"type": "string",
"description": "Hex colour for UIs, e.g. #3366ff"
},
"description": {
"type": "string",
"description": "Up to 500 chars"
}
}
}Create a short link that redirects to targetUrl, or shows a link page (pass page instead of targetUrl). Returns the link, including linkId (use it with the other tools) and shortUrl (share this). Give a slug for a memorable URL, otherwise a random 8-character one is generated. Slugs are unique across all tenants and cannot be changed later (409 if taken; route names like "api", "admin", "health" are reserved).
| Argument | Type | Description |
|---|---|---|
| name | string | Human label, up to 200 chars |
| page | object | A link page (link in bio): instead of redirecting, the short URL shows a page with a title, an optional avatar and bio, a list of buttons and a row of social icons, styled by a theme. Rules still apply first: a matching rule redirects. Shape: {title (≤80), bio? (≤300), avatar? {data: "data:image/png;base64,..."} or {imageId} to keep the current one, items: [{type: "link", label, url} | {type: "link", label, linkId} (another of your links: opens its short URL) | {type: "header", text} | {type: "text", text}], socials?: [{platform, url}], theme?, language?, noindex?, footer?}. Items take id? (stable, clicks are counted by it; generated if omitted, keep it when editing), enabled? (default true), when? (a condition in the redirect-rule grammar: the item shows only to matching visits, e.g. {"field":"os","op":"eq","value":"ios"}), and link items highlight? (accent colour) and thumbnail? (a picture). Up to 50 items and 8 pictures. socials platforms: instagram, tiktok, youtube, x, facebook, linkedin, github, threads, bluesky, mastodon, whatsapp, telegram, spotify, twitch, discord, pinterest, snapchat, substack, email, website (email takes mailto:). theme: {preset? (light|dark|midnight|sunset|forest|pastel|mono, default light), background? {type: "solid", color} | {type: "gradient", from, to, angle?} | {type: "image", image: {data}, dim? 0-0.8}, text? (hex colour), font? (system|rounded|serif|mono|condensed), button? {shape? (square|rounded|pill), fill? (solid|outline|soft|shadow), color?, text?}, avatarShape? (circle|rounded|square), align? (center|left)}. Low-contrast colours come back as `warnings` but are saved. Pages of self-signed-up tenants carry a small "Made with esyURL · Report" footer; footer: false removes it for a one-off x402 payment per page (see payment). |
Delete a group. Its links are NOT deleted; they just stop being members.
| Argument | Type | Description |
|---|---|---|
| groupId* | string | The group id (groupId), as returned by create_group or list_groups |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groupId"
],
"properties": {
"groupId": {
"type": "string",
"minLength": 1,
"description": "The group id (groupId), as returned by create_group or list_groups"
}
}
}Permanently delete a short link and remove it from its groups. Its short URL and any printed QR codes stop working (404) and the slug becomes free for anyone. Prefer update_short_link status "paused" if the link may come back.
| Argument | Type | Description |
|---|---|---|
| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
}
}
}Get this tenant's custom (alias) domain and advance its setup as far as DNS allows. Use it to check progress after creating a DNS record. Lifecycle: pending_validation (create the certificate_validation record, and the optional ownership_verification TXT if listed) → provisioning (automatic, seconds) → pending_dns (create the routing CNAME from the domain to `target`) → active. Nothing advances on its own: call get_custom_domain after each DNS change to move it along. The response always carries `dnsRecords` (exact name/type/value to create, with `done` once seen) and `instructions` saying what to do next.
No arguments.
{
"type": "object",
"properties": {}
}Fetch one group. To see its links use list_short_links with group = groupId.
| Argument | Type | Description |
|---|---|---|
| groupId* | string | The group id (groupId), as returned by create_group or list_groups |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groupId"
],
"properties": {
"groupId": {
"type": "string",
"minLength": 1,
"description": "The group id (groupId), as returned by create_group or list_groups"
}
}
}Combined visit analytics for a group's current member links: totals {visits, qr, direct, bot} and a zero-filled per-day breakdown (UTC). Links removed from the group no longer count.
| Argument | Type | Description |
|---|---|---|
| days | integer | How many UTC days back, including today: 1-365, default 30 |
| groupId* | string | The group id (groupId), as returned by create_group or list_groups |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groupId"
],
"properties": {
"days": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "How many UTC days back, including today: 1-365, default 30"
},
"groupId": {
"type": "string",
"minLength": 1,
"description": "The group id (groupId), as returned by create_group or list_groups"
}
}
}Fetch one short link by linkId, including its groupIds and lifetime visitCount.
| Argument | Type | Description |
|---|---|---|
| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
}
}
}Visit analytics for one link: totals over the window ({visits, qr, direct, bot}; visits = qr + direct, bots such as link-preview crawlers are counted separately and excluded from visits), a zero-filled per-day breakdown (UTC, oldest first), and the most recent individual visits (newest first, with source, referrerHost, userAgent, country when known, and ruleId when a redirect rule decided it; kept 90 days). byRule splits the window's visits by rule (fallback = went to the link's targetUrl); daily entries carry byRule on days a rule matched. For link pages, byItem counts button clicks per item (daily entries carry clicks by item id).
| Argument | Type | Description |
|---|---|---|
| days | integer | How many UTC days back, including today: 1-365, default 30 |
| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
| recent | integer | How many recent visit events to include, 0-200, default 50 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"days": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "How many UTC days back, including today: 1-365, default 30"
},
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
},
"recent": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "How many recent visit events to include, 0-200, default 50"
}
}
}List your groups (with linkCount), ordered by groupId. Page with cursor = nextCursor.
| Argument | Type | Description |
|---|---|---|
| limit | integer | Page size 1-100, default 20 |
| cursor | string | nextCursor from the previous page; omit for the first page |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "Page size 1-100, default 20"
},
"cursor": {
"type": "string",
"description": "nextCursor from the previous page; omit for the first page"
}
}
}List your short links, newest first. Filter by tag and/or groupId. A page can hold fewer than limit items even when more exist: keep calling with cursor = nextCursor until nextCursor is absent.
| Argument | Type | Description |
|---|---|---|
| tag | string | Only links carrying this tag |
| group | string | Only links in this group (groupId) |
| limit | integer | Page size 1-100, default 20 |
| cursor | string | nextCursor from the previous page; omit for the first page |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"tag": {
"type": "string",
"description": "Only links carrying this tag"
},
"group": {
"type": "string",
"description": "Only links in this group (groupId)"
},
"limit": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "Page size 1-100, default 20"
},
"cursor": {
"type": "string",
"description": "nextCursor from the previous page; omit for the first page"
}
}
}Render a link's page as HTML for a simulated visit, without recording anything: the saved page, or a draft (page) to try before saving (pictures given as data URLs are drawn inline; nothing is stored or charged). Returns the visible item ids (items with "when" conditions depend on the visit) and the HTML.
| Argument | Type | Description |
|---|---|---|
| at | string | ISO timestamp; default now |
| page | object | A draft to render instead of the saved page |
| query | object | |
| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
| source | string | |
| country | string | ISO 3166-1 alpha-2, e.g. "GB" |
| referrer | string | |
| userAgent | string | |
| acceptLanguage | string |
Remove this tenant's custom domain. Short links immediately stop resolving on it (QR codes printed with it break) and new short URLs use the platform domain. The DNS records can be deleted afterwards.
No arguments.
{
"type": "object",
"properties": {}
}Take one link out of a group. The link itself is untouched.
| Argument | Type | Description |
|---|---|---|
| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
| groupId* | string | The group id (groupId), as returned by create_group or list_groups |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groupId",
"linkId"
],
"properties": {
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
},
"groupId": {
"type": "string",
"minLength": 1,
"description": "The group id (groupId), as returned by create_group or list_groups"
}
}
}Render a QR code as a PNG image. Pass linkId to render a short link: the image encodes {shortUrl}?s=qr so scans are counted as qr visits, and the link's qrStyle is the default look. Or pass data to render any text (URL, Wi-Fi config, vCard...) as a static QR code that is not tracked. Returns the image plus a text line naming exactly what was encoded. Styles are free: module shapes, corner markers, a logo or a halftone photo. For SVG, use GET /v1/links/{linkId}/qr?format=svg.
| Argument | Type | Description |
|---|---|---|
| ecl | string | QR error correction level: L 7%, M 15% (default), Q 25%, H 30% |
| dark | string | Hex colour, e.g. #000000 (the # is optional) |
| data | string | Arbitrary text to encode, up to 2048 chars (exclusive with linkId) |
| size | integer | Width in pixels, 64-2048, default 512 |
| image | object | For data only (a link uses its saved qrStyle). A picture in the code. mode "logo": in the middle (error correction becomes H; size is its side as a share of the code, 0.1-0.3, default 0.22). mode "halftone": a photo behind the code, drawn as dots over it. data is a data URL (data:image/png;base64,... png, jpeg, webp or gif, up to 2 MB). |
| light | string | Hex colour, e.g. #000000 (the # is optional) |
| linkId |
Serve all of this tenant's short links on a domain it controls, e.g. "go.acme.com" → https://go.acme.com/{slug}. Once active, new short URLs and QR codes use it; links keep working on the platform domain too. One domain per tenant: setting a different one replaces (and tears down) the current one. Setting the same one again is harmless, and restarts it if it failed. Two DNS records are needed at the domain's DNS provider, returned one at a time: first a record (TXT or CNAME) that proves control (for the TLS certificate), then a CNAME pointing the domain at the platform. Lifecycle: pending_validation (create the certificate_validation record, and the optional ownership_verification TXT if listed) → provisioning (automatic, seconds) → pending_dns (create the routing CNAME from the domain to `target`) → active. Nothing advances on its own: call get_custom_domain after each DNS change to move it along. The response always carries `dnsRecords` (exact name/type/value to create, with `done` once seen) and `instructions` saying what to do next. Self-signed-up tenants (see /auth.md) pay once per new domain with x402: without a payment the tool returns an error whose structuredContent is an x402 PaymentRequired (x402Version 2, exact scheme, USDC). Retry with the signed PaymentPayload in the request's _meta["x402/payment"] (the x402 MCP transport), or, if your client cannot set _meta, in the `payment` argument. The settlement comes back in _meta["x402/payment-response"]. Re-setting the current domain, reading and removing are free; tenants given a key by an operator never pay.
| Argument | Type | Description |
|---|---|---|
| domain* | string | Bare hostname you control, e.g. go.acme.com - no scheme, port or path |
| payment | any | Only if your client cannot send _meta["x402/payment"]: the x402 PaymentPayload, as an object or base64 |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"domain"
],
"properties": {
"domain": {
"type": "string",
"description": "Bare hostname you control, e.g. go.acme.com - no scheme, port or path"
},
"payment": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
],
"description": "Only if your client cannot send _meta[\"x402/payment\"]: the x402 PaymentPayload, as an object or base64"
}
}
}Choose the link that the root (/) of this tenant's custom domain shows: a link page becomes the domain's home page, a redirect link sends / there. Pass linkId null to clear it (/ answers 404 again). Has no effect on the platform domain.
| Argument | Type | Description |
|---|---|---|
| linkId* | string | null | The link to show at /, or null to clear |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"linkId": {
"type": [
"string",
"null"
],
"description": "The link to show at /, or null to clear"
}
}
}Dry run: which destination would a visit with these properties get from this link's rules? For a link page with no matching rule it returns page.itemIds (the buttons that visit would see) instead of targetUrl. Records no visit. Pass a real userAgent to test device routing, e.g. iPhone: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", Android: "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Mobile Safari/537.36". Returns targetUrl, matchedRule (absent when the link's targetUrl is used) and the context the rules saw (os, device, browser, language, country, referrer, source, query, at).
| Argument | Type | Description |
|---|---|---|
| at | string | ISO timestamp to evaluate time/weekday/hour rules at; default now |
| query | object | Query parameters on the short URL |
| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
| source | string | qr simulates a scanned QR code; default direct |
| country | string | ISO 3166-1 alpha-2 country the visitor is in, e.g. "GB" |
| referrer | string | Full referring URL, e.g. https://www.instagram.com/ |
Rename a group or change its description or color. Membership doesn't change here.
| Argument | Type | Description |
|---|---|---|
| name | string | Group name, up to 100 chars |
| color | string | Hex colour for UIs, e.g. #3366ff |
| groupId* | string | The group id (groupId), as returned by create_group or list_groups |
| description | string | Up to 500 chars |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"groupId"
],
"properties": {
"name": {
"type": "string",
"description": "Group name, up to 100 chars"
},
"color": {
"type": "string",
"description": "Hex colour for UIs, e.g. #3366ff"
},
"groupId": {
"type": "string",
"minLength": 1,
"description": "The group id (groupId), as returned by create_group or list_groups"
},
"description": {
"type": "string",
"description": "Up to 500 chars"
}
}
}Change a short link. Only the fields you pass change; tags, metadata, qrStyle and groupIds are replaced wholesale (pass the full new list; [] clears). Set targetUrl to send visitors somewhere else: the short URL and printed QR codes keep working. Set status "paused" to make the short URL answer 410, "active" to resume. The slug cannot be changed. page replaces the whole link page (a redirect becomes a page); page: null with a targetUrl turns a page back into a redirect.
| Argument | Type | Description |
|---|---|---|
| name | string | |
| page | any | |
| tags | array | Free-form labels, up to 20; filter with list_short_links tag |
| rules | array | The complete new rule list; replaces the current one ([] removes all). Redirect rules, evaluated in order on every visit; the first enabled rule whose "when" matches sends the visitor to its targetUrl, otherwise the link's targetUrl is used. A condition is either a test {field, op, value} or a combination {all: [...]}, {any: [...]}, {not: {...}}. Fields and their ops: os (ios|android|windows|macos|linux|chromeos|other), device (mobile|tablet|desktop|bot), browser (safari|chrome|firefox|edge|samsung|opera|in_app|other), source (qr|direct|bot): eq, neq, in, not_in. language (Accept-Language primary tag; "pt" also matches "pt-BR"): eq, neq, in, not_in, exists. country (ISO 3166-1 alpha-2, e.g. "GB", geolocated by the edge on every host): eq, neq, in, not_in. An unknown or absent value fails eq/in and passes neq/not_in. referrer (host, e.g. instagram.com) and query (needs "key", the parameter name): eq, neq, in, not_in, contains, starts_with, ends_with, exists. time (ISO timestamp): before, after. weekday (sun..sat, UTC): eq, neq, in, not_in. hour (0-23 UTC): eq, neq, in, not_in, gte, lte. in/not_in take an array. Text compares case-insensitively. Up to 20 rules, 20 conditions per rule. Example app-store split: [{"name":"iOS","when":{"field":"os","op":"eq","value":"ios"},"targetUrl":"https://apps.apple.com/app/id123"},{"name":"Android","when":{"field":"os","op":"eq","value":"android"},"targetUrl":"https://play.google.com/store/apps/details?id=com.example"}] |
| slug | string | Custom slug, ^[a-zA-Z0-9_-]{3,64}$; random if omitted |
| tags | array | Free-form labels, up to 20; filter with list_short_links tag |
| rules | array | Redirect rules, evaluated in order on every visit; the first enabled rule whose "when" matches sends the visitor to its targetUrl, otherwise the link's targetUrl is used. A condition is either a test {field, op, value} or a combination {all: [...]}, {any: [...]}, {not: {...}}. Fields and their ops: os (ios|android|windows|macos|linux|chromeos|other), device (mobile|tablet|desktop|bot), browser (safari|chrome|firefox|edge|samsung|opera|in_app|other), source (qr|direct|bot): eq, neq, in, not_in. language (Accept-Language primary tag; "pt" also matches "pt-BR"): eq, neq, in, not_in, exists. country (ISO 3166-1 alpha-2, e.g. "GB", geolocated by the edge on every host): eq, neq, in, not_in. An unknown or absent value fails eq/in and passes neq/not_in. referrer (host, e.g. instagram.com) and query (needs "key", the parameter name): eq, neq, in, not_in, contains, starts_with, ends_with, exists. time (ISO timestamp): before, after. weekday (sun..sat, UTC): eq, neq, in, not_in. hour (0-23 UTC): eq, neq, in, not_in, gte, lte. in/not_in take an array. Text compares case-insensitively. Up to 20 rules, 20 conditions per rule. Example app-store split: [{"name":"iOS","when":{"field":"os","op":"eq","value":"ios"},"targetUrl":"https://apps.apple.com/app/id123"},{"name":"Android","when":{"field":"os","op":"eq","value":"android"},"targetUrl":"https://play.google.com/store/apps/details?id=com.example"}] |
| payment | any | Only for footer: false on a page that has not been paid off, by a self-signed-up tenant: the tool first returns an error whose structuredContent is an x402 PaymentRequired; retry with the signed PaymentPayload in _meta["x402/payment"], or, if your client cannot set _meta, here (an object or base64). |
| qrStyle | object | Default look of this link's QR image (free): colours, module and corner-marker shapes, and an optional picture. Styled codes need dark on light with at least 3:1 contrast. The whole style is replaced on update. |
| groupIds | array | Ids of groups (from create_group/list_groups) this link belongs to, up to 20 |
| metadata | object | Up to 20 string key/value pairs for your own bookkeeping |
| targetUrl | string | Absolute http(s) URL visitors are redirected to (exclusive with page) |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"name": {
"type": "string",
"description": "Human label, up to 200 chars"
},
"page": {
"type": "object",
"description": "A link page (link in bio): instead of redirecting, the short URL shows a page with a title, an optional avatar and bio, a list of buttons and a row of social icons, styled by a theme. Rules still apply first: a matching rule redirects. Shape: {title (≤80), bio? (≤300), avatar? {data: \"data:image/png;base64,...\"} or {imageId} to keep the current one, items: [{type: \"link\", label, url} | {type: \"link\", label, linkId} (another of your links: opens its short URL) | {type: \"header\", text} | {type: \"text\", text}], socials?: [{platform, url}], theme?, language?, noindex?, footer?}. Items take id? (stable, clicks are counted by it; generated if omitted, keep it when editing), enabled? (default true), when? (a condition in the redirect-rule grammar: the item shows only to matching visits, e.g. {\"field\":\"os\",\"op\":\"eq\",\"value\":\"ios\"}), and link items highlight? (accent colour) and thumbnail? (a picture). Up to 50 items and 8 pictures. socials platforms: instagram, tiktok, youtube, x, facebook, linkedin, github, threads, bluesky, mastodon, whatsapp, telegram, spotify, twitch, discord, pinterest, snapchat, substack, email, website (email takes mailto:). theme: {preset? (light|dark|midnight|sunset|forest|pastel|mono, default light), background? {type: \"solid\", color} | {type: \"gradient\", from, to, angle?} | {type: \"image\", image: {data}, dim? 0-0.8}, text? (hex colour), font? (system|rounded|serif|mono|condensed), button? {shape? (square|rounded|pill), fill? (solid|outline|soft|shadow), color?, text?}, avatarShape? (circle|rounded|square), align? (center|left)}. Low-contrast colours come back as `warnings` but are saved. Pages of self-signed-up tenants carry a small \"Made with esyURL · Report\" footer; footer: false removes it for a one-off x402 payment per page (see payment).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"slug": {
"type": "string",
"description": "Custom slug, ^[a-zA-Z0-9_-]{3,64}$; random if omitted"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Free-form labels, up to 20; filter with list_short_links tag"
},
"rules": {
"type": "array",
"items": {
"type": "object",
"required": [
"when",
"targetUrl"
],
"properties": {
"id": {
"type": "string",
"description": "Stable id, shown on visits; generated if omitted. Keep it when editing a rule"
},
"name": {
"type": "string"
},
"when": {
"type": "object",
"description": "The condition, e.g. {\"field\":\"os\",\"op\":\"eq\",\"value\":\"ios\"}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"enabled": {
"type": "boolean",
"description": "Default true; false keeps the rule but skips it"
},
"targetUrl": {
"type": "string",
"description": "Absolute http(s) destination when this rule matches"
}
}
},
"description": "Redirect rules, evaluated in order on every visit; the first enabled rule whose \"when\" matches sends the visitor to its targetUrl, otherwise the link's targetUrl is used. A condition is either a test {field, op, value} or a combination {all: [...]}, {any: [...]}, {not: {...}}. Fields and their ops: os (ios|android|windows|macos|linux|chromeos|other), device (mobile|tablet|desktop|bot), browser (safari|chrome|firefox|edge|samsung|opera|in_app|other), source (qr|direct|bot): eq, neq, in, not_in. language (Accept-Language primary tag; \"pt\" also matches \"pt-BR\"): eq, neq, in, not_in, exists. country (ISO 3166-1 alpha-2, e.g. \"GB\", geolocated by the edge on every host): eq, neq, in, not_in. An unknown or absent value fails eq/in and passes neq/not_in. referrer (host, e.g. instagram.com) and query (needs \"key\", the parameter name): eq, neq, in, not_in, contains, starts_with, ends_with, exists. time (ISO timestamp): before, after. weekday (sun..sat, UTC): eq, neq, in, not_in. hour (0-23 UTC): eq, neq, in, not_in, gte, lte. in/not_in take an array. Text compares case-insensitively. Up to 20 rules, 20 conditions per rule. Example app-store split: [{\"name\":\"iOS\",\"when\":{\"field\":\"os\",\"op\":\"eq\",\"value\":\"ios\"},\"targetUrl\":\"https://apps.apple.com/app/id123\"},{\"name\":\"Android\",\"when\":{\"field\":\"os\",\"op\":\"eq\",\"value\":\"android\"},\"targetUrl\":\"https://play.google.com/store/apps/details?id=com.example\"}]"
},
"payment": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
],
"description": "Only for footer: false on a page that has not been paid off, by a self-signed-up tenant: the tool first returns an error whose structuredContent is an x402 PaymentRequired; retry with the signed PaymentPayload in _meta[\"x402/payment\"], or, if your client cannot set _meta, here (an object or base64)."
},
"qrStyle": {
"type": "object",
"properties": {
"dark": {
"type": "string",
"description": "Hex colour, e.g. #000000 (the # is optional)"
},
"image": {
"type": "object",
"required": [
"mode"
],
"properties": {
"data": {
"type": "string",
"description": "A new picture, as a data URL"
},
"mode": {
"enum": [
"logo",
"halftone"
],
"type": "string"
},
"size": {
"type": "number"
},
"imageId": {
"type": "string",
"description": "Keep the picture the link already has (its qrStyle.image.imageId)"
}
},
"description": "A picture in the code. mode \"logo\": in the middle (error correction becomes H; size is its side as a share of the code, 0.1-0.3, default 0.22). mode \"halftone\": a photo behind the code, drawn as dots over it. data is a data URL (data:image/png;base64,... png, jpeg, webp or gif, up to 2 MB). To keep the current picture while changing other fields, pass its imageId instead of data."
},
"light": {
"type": "string",
"description": "Hex colour, e.g. #000000 (the # is optional)"
},
"margin": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "Quiet zone in modules, 0-16"
},
"eyeBall": {
"enum": [
"square",
"rounded",
"circle"
],
"type": "string",
"description": "Centre of the corner markers"
},
"modules": {
"enum": [
"square",
"rounded",
"dot",
"row",
"column"
],
"type": "string",
"description": "Shape of the data modules (default square)"
},
"eyeColor": {
"type": "string",
"description": "Colour of the corner markers; defaults to dark"
},
"eyeFrame": {
"enum": [
"square",
"rounded",
"circle"
],
"type": "string",
"description": "Outer ring of the three corner markers"
},
"errorCorrectionLevel": {
"enum": [
"L",
"M",
"Q",
"H"
],
"type": "string",
"description": "QR error correction level: L 7%, M 15% (default), Q 25%, H 30%"
}
},
"description": "Default look of this link's QR image (free): colours, module and corner-marker shapes, and an optional picture. Styled codes need dark on light with at least 3:1 contrast. The whole style is replaced on update."
},
"groupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Ids of groups (from create_group/list_groups) this link belongs to, up to 20"
},
"metadata": {
"type": "object",
"description": "Up to 20 string key/value pairs for your own bookkeeping",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"targetUrl": {
"type": "string",
"description": "Absolute http(s) URL visitors are redirected to (exclusive with page)"
}
}
}{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"at": {
"type": "string",
"description": "ISO timestamp; default now"
},
"page": {
"type": "object",
"description": "A draft to render instead of the saved page",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"query": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
},
"source": {
"enum": [
"qr",
"direct"
],
"type": "string"
},
"country": {
"type": "string",
"description": "ISO 3166-1 alpha-2, e.g. \"GB\""
},
"referrer": {
"type": "string"
},
"userAgent": {
"type": "string"
},
"acceptLanguage": {
"type": "string"
}
}
}| string |
| Render this short link (exclusive with data) |
| margin | integer | Quiet zone in modules, 0-16, default 4 |
| eyeBall | string | Centre of the corner markers |
| modules | string | Shape of the data modules (default square) |
| eyeColor | string | Colour of the corner markers; defaults to dark |
| eyeFrame | string | Outer ring of the three corner markers |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"ecl": {
"enum": [
"L",
"M",
"Q",
"H"
],
"type": "string",
"description": "QR error correction level: L 7%, M 15% (default), Q 25%, H 30%"
},
"dark": {
"type": "string",
"description": "Hex colour, e.g. #000000 (the # is optional)"
},
"data": {
"type": "string",
"description": "Arbitrary text to encode, up to 2048 chars (exclusive with linkId)"
},
"size": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "Width in pixels, 64-2048, default 512"
},
"image": {
"type": "object",
"required": [
"mode",
"data"
],
"properties": {
"data": {
"type": "string"
},
"mode": {
"enum": [
"logo",
"halftone"
],
"type": "string"
},
"size": {
"type": "number"
}
},
"description": "For data only (a link uses its saved qrStyle). A picture in the code. mode \"logo\": in the middle (error correction becomes H; size is its side as a share of the code, 0.1-0.3, default 0.22). mode \"halftone\": a photo behind the code, drawn as dots over it. data is a data URL (data:image/png;base64,... png, jpeg, webp or gif, up to 2 MB)."
},
"light": {
"type": "string",
"description": "Hex colour, e.g. #000000 (the # is optional)"
},
"linkId": {
"type": "string",
"minLength": 1,
"description": "Render this short link (exclusive with data)"
},
"margin": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "Quiet zone in modules, 0-16, default 4"
},
"eyeBall": {
"enum": [
"square",
"rounded",
"circle"
],
"type": "string",
"description": "Centre of the corner markers"
},
"modules": {
"enum": [
"square",
"rounded",
"dot",
"row",
"column"
],
"type": "string",
"description": "Shape of the data modules (default square)"
},
"eyeColor": {
"type": "string",
"description": "Colour of the corner markers; defaults to dark"
},
"eyeFrame": {
"enum": [
"square",
"rounded",
"circle"
],
"type": "string",
"description": "Outer ring of the three corner markers"
}
}
}| userAgent |
| string |
| acceptLanguage | string | e.g. "pt-BR,pt;q=0.9,en;q=0.8" |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"at": {
"type": "string",
"description": "ISO timestamp to evaluate time/weekday/hour rules at; default now"
},
"query": {
"type": "object",
"description": "Query parameters on the short URL",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
},
"source": {
"enum": [
"qr",
"direct"
],
"type": "string",
"description": "qr simulates a scanned QR code; default direct"
},
"country": {
"type": "string",
"description": "ISO 3166-1 alpha-2 country the visitor is in, e.g. \"GB\""
},
"referrer": {
"type": "string",
"description": "Full referring URL, e.g. https://www.instagram.com/"
},
"userAgent": {
"type": "string"
},
"acceptLanguage": {
"type": "string",
"description": "e.g. \"pt-BR,pt;q=0.9,en;q=0.8\""
}
}
}| linkId* | string | The link id (linkId), as returned by create_short_link or list_short_links |
| status | string |
| payment | any | Only for footer: false on a page that has not been paid off, by a self-signed-up tenant: the tool first returns an error whose structuredContent is an x402 PaymentRequired; retry with the signed PaymentPayload in _meta["x402/payment"], or, if your client cannot set _meta, here (an object or base64). |
| qrStyle | object | Default look of this link's QR image (free): colours, module and corner-marker shapes, and an optional picture. Styled codes need dark on light with at least 3:1 contrast. The whole style is replaced on update. |
| groupIds | array | The complete new set of groups; replaces the current one |
| metadata | object | Up to 20 string key/value pairs for your own bookkeeping |
| targetUrl | string | null | New absolute http(s) destination |
{
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"required": [
"linkId"
],
"properties": {
"name": {
"type": "string"
},
"page": {
"anyOf": [
{
"type": "object",
"description": "A link page (link in bio): instead of redirecting, the short URL shows a page with a title, an optional avatar and bio, a list of buttons and a row of social icons, styled by a theme. Rules still apply first: a matching rule redirects. Shape: {title (≤80), bio? (≤300), avatar? {data: \"data:image/png;base64,...\"} or {imageId} to keep the current one, items: [{type: \"link\", label, url} | {type: \"link\", label, linkId} (another of your links: opens its short URL) | {type: \"header\", text} | {type: \"text\", text}], socials?: [{platform, url}], theme?, language?, noindex?, footer?}. Items take id? (stable, clicks are counted by it; generated if omitted, keep it when editing), enabled? (default true), when? (a condition in the redirect-rule grammar: the item shows only to matching visits, e.g. {\"field\":\"os\",\"op\":\"eq\",\"value\":\"ios\"}), and link items highlight? (accent colour) and thumbnail? (a picture). Up to 50 items and 8 pictures. socials platforms: instagram, tiktok, youtube, x, facebook, linkedin, github, threads, bluesky, mastodon, whatsapp, telegram, spotify, twitch, discord, pinterest, snapchat, substack, email, website (email takes mailto:). theme: {preset? (light|dark|midnight|sunset|forest|pastel|mono, default light), background? {type: \"solid\", color} | {type: \"gradient\", from, to, angle?} | {type: \"image\", image: {data}, dim? 0-0.8}, text? (hex colour), font? (system|rounded|serif|mono|condensed), button? {shape? (square|rounded|pill), fill? (solid|outline|soft|shadow), color?, text?}, avatarShape? (circle|rounded|square), align? (center|left)}. Low-contrast colours come back as `warnings` but are saved. Pages of self-signed-up tenants carry a small \"Made with esyURL · Report\" footer; footer: false removes it for a one-off x402 payment per page (see payment).",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
{
"type": "null"
}
]
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Free-form labels, up to 20; filter with list_short_links tag"
},
"rules": {
"type": "array",
"items": {
"type": "object",
"required": [
"when",
"targetUrl"
],
"properties": {
"id": {
"type": "string",
"description": "Stable id, shown on visits; generated if omitted. Keep it when editing a rule"
},
"name": {
"type": "string"
},
"when": {
"type": "object",
"description": "The condition, e.g. {\"field\":\"os\",\"op\":\"eq\",\"value\":\"ios\"}",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
},
"enabled": {
"type": "boolean",
"description": "Default true; false keeps the rule but skips it"
},
"targetUrl": {
"type": "string",
"description": "Absolute http(s) destination when this rule matches"
}
}
},
"description": "The complete new rule list; replaces the current one ([] removes all). Redirect rules, evaluated in order on every visit; the first enabled rule whose \"when\" matches sends the visitor to its targetUrl, otherwise the link's targetUrl is used. A condition is either a test {field, op, value} or a combination {all: [...]}, {any: [...]}, {not: {...}}. Fields and their ops: os (ios|android|windows|macos|linux|chromeos|other), device (mobile|tablet|desktop|bot), browser (safari|chrome|firefox|edge|samsung|opera|in_app|other), source (qr|direct|bot): eq, neq, in, not_in. language (Accept-Language primary tag; \"pt\" also matches \"pt-BR\"): eq, neq, in, not_in, exists. country (ISO 3166-1 alpha-2, e.g. \"GB\", geolocated by the edge on every host): eq, neq, in, not_in. An unknown or absent value fails eq/in and passes neq/not_in. referrer (host, e.g. instagram.com) and query (needs \"key\", the parameter name): eq, neq, in, not_in, contains, starts_with, ends_with, exists. time (ISO timestamp): before, after. weekday (sun..sat, UTC): eq, neq, in, not_in. hour (0-23 UTC): eq, neq, in, not_in, gte, lte. in/not_in take an array. Text compares case-insensitively. Up to 20 rules, 20 conditions per rule. Example app-store split: [{\"name\":\"iOS\",\"when\":{\"field\":\"os\",\"op\":\"eq\",\"value\":\"ios\"},\"targetUrl\":\"https://apps.apple.com/app/id123\"},{\"name\":\"Android\",\"when\":{\"field\":\"os\",\"op\":\"eq\",\"value\":\"android\"},\"targetUrl\":\"https://play.google.com/store/apps/details?id=com.example\"}]"
},
"linkId": {
"type": "string",
"minLength": 1,
"description": "The link id (linkId), as returned by create_short_link or list_short_links"
},
"status": {
"enum": [
"active",
"paused"
],
"type": "string"
},
"payment": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
],
"description": "Only for footer: false on a page that has not been paid off, by a self-signed-up tenant: the tool first returns an error whose structuredContent is an x402 PaymentRequired; retry with the signed PaymentPayload in _meta[\"x402/payment\"], or, if your client cannot set _meta, here (an object or base64)."
},
"qrStyle": {
"type": "object",
"properties": {
"dark": {
"type": "string",
"description": "Hex colour, e.g. #000000 (the # is optional)"
},
"image": {
"type": "object",
"required": [
"mode"
],
"properties": {
"data": {
"type": "string",
"description": "A new picture, as a data URL"
},
"mode": {
"enum": [
"logo",
"halftone"
],
"type": "string"
},
"size": {
"type": "number"
},
"imageId": {
"type": "string",
"description": "Keep the picture the link already has (its qrStyle.image.imageId)"
}
},
"description": "A picture in the code. mode \"logo\": in the middle (error correction becomes H; size is its side as a share of the code, 0.1-0.3, default 0.22). mode \"halftone\": a photo behind the code, drawn as dots over it. data is a data URL (data:image/png;base64,... png, jpeg, webp or gif, up to 2 MB). To keep the current picture while changing other fields, pass its imageId instead of data."
},
"light": {
"type": "string",
"description": "Hex colour, e.g. #000000 (the # is optional)"
},
"margin": {
"type": "integer",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"description": "Quiet zone in modules, 0-16"
},
"eyeBall": {
"enum": [
"square",
"rounded",
"circle"
],
"type": "string",
"description": "Centre of the corner markers"
},
"modules": {
"enum": [
"square",
"rounded",
"dot",
"row",
"column"
],
"type": "string",
"description": "Shape of the data modules (default square)"
},
"eyeColor": {
"type": "string",
"description": "Colour of the corner markers; defaults to dark"
},
"eyeFrame": {
"enum": [
"square",
"rounded",
"circle"
],
"type": "string",
"description": "Outer ring of the three corner markers"
},
"errorCorrectionLevel": {
"enum": [
"L",
"M",
"Q",
"H"
],
"type": "string",
"description": "QR error correction level: L 7%, M 15% (default), Q 25%, H 30%"
}
},
"description": "Default look of this link's QR image (free): colours, module and corner-marker shapes, and an optional picture. Styled codes need dark on light with at least 3:1 contrast. The whole style is replaced on update."
},
"groupIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "The complete new set of groups; replaces the current one"
},
"metadata": {
"type": "object",
"description": "Up to 20 string key/value pairs for your own bookkeeping",
"propertyNames": {
"type": "string"
},
"additionalProperties": {
"type": "string"
}
},
"targetUrl": {
"type": [
"string",
"null"
],
"description": "New absolute http(s) destination"
}
}
}