# API Reference Integrate Viso.li into your application with a small REST API. Create short links, read click stats, and track campaign goals. ## Authentication Authenticate with an API key from your dashboard. Keys are available on **PRO** and **ENTERPRISE** plans. Send the key as a Bearer token. Requests without `Authorization`, or over plain HTTP, fail. Header format: `Authorization: Bearer sk_api_your_api_key` ## Rate limiting The limit is **10 requests per minute** and **50 requests per day**. Exceeding it returns `429 Too Many Requests`. ## Endpoints Create Account, Verify Account, and Plans are public and limited by client IP. Every other endpoint uses the Bearer key. | Method | Path | Summary | | --- | --- | --- | | GET | /api/account | Read the active plan, monthly link limit, and links created this month. | | POST | /api/account/register | Create an account and email a verification code. | | POST | /api/account/verify | Confirm an account with the emailed 6-digit code. | | GET | /api/link/:slug | Read slug, hostname, alias, createdAt, and expiredAt for one short link. | | GET | /api/link | List your links. Filter by hostname and createdAt window. | | GET | /api/link/:slug/stats | Read click history and visitor details for a short link. | | GET | /api/link/:slug/stats/summary | Read visit, visitor, country, device, and referrer counts for a short link. | | POST | /api/link/create | Shorten a destination URL, optionally on a custom domain. | | DELETE | /api/link/:slug | Delete one short link you own. | | GET | /api/domain | List active custom domains you can use when creating links. | | GET | /api/campaign/:id | Read one campaign, including goal, progress, and attached links. | | GET | /api/campaign | List your campaigns. Filter by createdAt, status, or template. | | POST | /api/campaign/create | Group existing links around a clicks, visitors, or compare goal. | | GET | /api/campaign/:id/status | Read stored goal config and how close a campaign is to its target. | | DELETE | /api/campaign/:id | Delete a campaign. Attached links are kept. | | POST | /api/campaign/:id/links | Add existing links to a campaign. | | DELETE | /api/campaign/:id/links/:slug | Remove one link from a campaign. | | GET | /api/plans | List the public plan catalog and feature bullets. | # Account `GET /api/account` Returns the plan this API key is entitled to, how many links were created this calendar month, and whether the next create will be rejected for hitting the limit. `plan.type` is the entitled plan. A lapsed paid subscription is reported as `FREE`. ## Example request ```sh curl https://www.viso.li/api/account \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/account", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. ```json { "plan": { "type": "PRO", "period": "MONTHLY" }, "links": { "used": 12, "limit": 500, "limitReached": false } } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | plan.type | string | yes | FREE, STARTER, PRO, or ENTERPRISE. A lapsed paid subscription is reported as FREE. | | plan.period | string | yes | MONTHLY or YEARLY. | | links.used | number | yes | Links created during the current calendar month. | | links.limit | number | yes | Monthly link allowance for the entitled plan. | | links.limitReached | boolean | yes | True when used is at or above the monthly limit. | ## Errors | Status | Error | Description | | --- | --- | --- | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Create Account `POST /api/account/register` Creates an account for a name and email, then emails a 6-digit verification code. This call does not sign the user in and does not return an API key. The name is 2–50 characters and may contain only letters and spaces. ## Request body Send a JSON object. | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | yes | Display name. 2–50 characters. Letters and spaces only. | | email | string | yes | Address that receives the verification code. Max 125 characters. | ## Example request This creates an account for ada@example.com and emails a verification code. ```sh curl -X POST https://www.viso.li/api/account/register \ -H "Content-Type: application/json" \ -d '{ "name": "Ada Lovelace", "email": "ada@example.com" }' ``` ```js fetch("https://www.viso.li/api/account/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ "name": "Ada Lovelace", "email": "ada@example.com" }) }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `201 Created`. The account still needs the emailed code. This response does not include a session or an API key. ```json { "email": "ada@example.com", "verificationRequired": true } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | email | string | yes | Address the verification code was sent to. | | verificationRequired | boolean | yes | The account still needs the emailed code. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The body is not JSON, or the name or email fails validation. | | 409 | Conflict | An account with this email already exists. | | 429 | Too Many Requests | More than 2 requests per minute or 5 requests per day from this IP. The response includes a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Verify Account `POST /api/account/verify` Checks the 6-digit code emailed by Create Account and marks the account verified. This call does not sign the user in and does not return an API key. ## Request body Send a JSON object. | Field | Type | Required | Description | | --- | --- | --- | --- | | email | string | yes | Address used when the account was created. | | code | string | yes | Six-digit code from the verification email. | ## Example request This confirms the code emailed to ada@example.com. ```sh curl -X POST https://www.viso.li/api/account/verify \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "code": "123456" }' ``` ```js fetch("https://www.viso.li/api/account/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ "email": "ada@example.com", "code": "123456" }) }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. The code was accepted. This response does not include a session or an API key. ```json { "email": "ada@example.com", "verified": true } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | email | string | yes | Address that was verified. | | verified | boolean | yes | The verification code was accepted. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The body is not JSON, the code is not 6 digits, or the code is invalid or expired. | | 429 | Too Many Requests | More than 2 requests per minute or 10 requests per day from this IP. The response includes a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Link `GET /api/link/:slug` Returns one short link you own: its slug, the hostname it belongs to, the optional alias, when it was created, and when it expires. `domain` is the hostname. It is `null` for viso.li links and a custom hostname such as `go.example.com` when the link is on a branded domain. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | Public slug of the shortened link. | ## Example request ```sh curl https://www.viso.li/api/link/abc123 \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/link/abc123", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. ```json { "slug": "abc123", "domain": "go.example.com", "alias": "launch", "createdAt": "2026-08-22T14:00:00.000Z", "expiredAt": "2026-12-31T23:59:59.000Z", "url": "https://example.com/bio" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | Encoded short-link identifier. | | domain | string \| null | yes | Hostname the link belongs to, or null for viso.li. | | alias | string \| null | yes | Custom back-half on a branded domain, if set. | | createdAt | string | yes | ISO 8601 time the link was created. | | expiredAt | string \| null | yes | ISO 8601 time the link stops redirecting, or null when it does not expire. | | url | string | yes | Destination URL. | ## Errors | Status | Error | Description | | --- | --- | --- | | 404 | Not Found | The slug does not exist or does not belong to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # All Links `GET /api/link` Returns a paginated list of short links you own. Each item has the same shape as GET link: slug, hostname, alias, createdAt, expiredAt, and destination URL. Filter by hostname and by an optional createdAt window so you can pull links created in a date range. ## Query parameters All filters are optional. Omit the createdAt window to include every link you own (still paginated). | Field | Type | Required | Description | | --- | --- | --- | --- | | domain | string | no | Hostname to filter on. Use viso.li for default-domain links, or a verified custom hostname. | | createdFrom | string | no | ISO 8601 inclusive start of the createdAt window. | | createdTo | string | no | ISO 8601 inclusive end of the createdAt window. Must be on or after createdFrom. | | page | number | no | Page number, starting at 1. Defaults to 1. | | pageSize | number | no | Items per page, max 100. Defaults to 20. | ## Example request This lists branded-domain links created between 1 January and 23 August 2026. ```sh curl "https://www.viso.li/api/link?domain=go.example.com&createdFrom=2026-01-01T00:00:00.000Z&createdTo=2026-08-23T23:59:59.000Z&page=1&pageSize=20" \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/link?domain=go.example.com&createdFrom=2026-01-01T00:00:00.000Z&createdTo=2026-08-23T23:59:59.000Z&page=1&pageSize=20", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. Newest links first. ```json { "links": [ { "slug": "abc123", "domain": "go.example.com", "alias": "launch", "createdAt": "2026-08-22T14:00:00.000Z", "expiredAt": "2026-12-31T23:59:59.000Z", "url": "https://example.com/bio" } ], "page": 1, "pageSize": 20, "total": 1, "totalPages": 1 } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | links | object[] | yes | Page of links. Same fields as GET /api/link/{slug}. | | links[].slug | string | yes | Encoded short-link identifier. | | links[].domain | string \| null | yes | Hostname the link belongs to, or null for viso.li. | | links[].alias | string \| null | yes | Custom back-half on a branded domain, if set. | | links[].createdAt | string | yes | ISO 8601 time the link was created. | | links[].expiredAt | string \| null | yes | ISO 8601 time the link stops redirecting, or null when it does not expire. | | links[].url | string | yes | Destination URL. | | page | number | yes | Current page. | | pageSize | number | yes | Requested page size. | | total | number | yes | Total matching links. | | totalPages | number | yes | Total pages at this page size. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | Invalid page, pageSize, or createdAt window. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Link Stats `GET /api/link/:slug/stats` Returns click history for a short link you own. Each invocation can include location, referrer, device, and network details, depending on your plan. Without dates, the range is the last 7 days through today (start of day, server time). ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | Public slug of the shortened link. | ## Query parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | startDate | string | no | ISO 8601 start of the range. Defaults to 7 days ago. | | endDate | string | no | ISO 8601 end of the range. Defaults to today. | ## Example request This reads January 2025 clicks for one slug. ```sh curl "https://www.viso.li/api/link/your-link-slug/stats?startDate=2025-01-01T00:00:00.000Z&endDate=2025-01-31T23:59:59.000Z" \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/link/your-link-slug/stats?startDate=2025-01-01T00:00:00.000Z&endDate=2025-01-31T23:59:59.000Z", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. Field detail on each invocation depends on your subscription. ```json { "slug": "your-link-slug", "stats": { "invocations": [ { "date": "2025-09-24T14:20:00.000Z", "continent": "NA", "country": "United States", "region": "CA", "city": "San Francisco", "referer": "google.com", "asn": { "asn": "15169", "name": "Google LLC", "domain": "google.com", "type": "COM" }, "browser": "Chrome", "os": "macOS", "deviceModel": "MacBook Pro", "deviceType": "desktop" } ], "startDate": "2025-09-01T00:00:00.000Z", "endDate": "2025-09-30T23:59:59.000Z" } } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | The requested short-link slug. | | stats.invocations | object[] | yes | One row per click in the date range. | | stats.invocations[].date | string | yes | ISO 8601 time of the click. | | stats.invocations[].continent | string \| null | yes | Continent code, when the plan includes location. | | stats.invocations[].country | string \| null | yes | Country, when the plan includes location. | | stats.invocations[].region | string \| null | yes | Region, on PRO and ENTERPRISE. | | stats.invocations[].city | string \| null | yes | City, on PRO and ENTERPRISE. | | stats.invocations[].referer | string \| null | yes | Referring host, on PRO and ENTERPRISE. | | stats.invocations[].asn.asn | string \| null | yes | Autonomous system number. | | stats.invocations[].asn.name | string \| null | yes | Network name. | | stats.invocations[].asn.domain | string \| null | yes | Network domain. | | stats.invocations[].asn.type | string \| null | yes | Network type. Populated on ENTERPRISE. | | stats.invocations[].browser | string \| null | yes | Browser name. | | stats.invocations[].os | string \| null | yes | Operating system. | | stats.invocations[].deviceModel | string \| null | yes | Device model, on paid plans. | | stats.invocations[].deviceType | string \| null | yes | Device type, on paid plans. | | stats.startDate | string | yes | ISO 8601 start of the resolved range. | | stats.endDate | string | yes | ISO 8601 end of the resolved range. | ## Errors | Status | Error | Description | | --- | --- | --- | | 404 | Not Found | The slug does not exist or does not belong to you. | | 400 | Bad Request | startDate or endDate is not a valid ISO 8601 datetime, or endDate is before startDate. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Link Stats Summary `GET /api/link/:slug/stats/summary` Returns counted totals for a short link you own: visits, unique visitors, countries, and the breakdowns your plan includes. Without dates, the range is the last 7 days through today (start of day, server time). `deviceTypes` and `operatingSystems` are `null` on FREE. `referers.items` is `null` below PRO. `referers.unique` is always a number. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | Public slug of the shortened link. | ## Query parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | startDate | string | no | ISO 8601 start of the range. Defaults to 7 days ago. | | endDate | string | no | ISO 8601 end of the range. Defaults to today. | ## Example request This reads the counted overview for 12–19 October 2026. On FREE, `deviceTypes` and `operatingSystems` are null. Below PRO, `referers.items` is null. ```sh curl "https://www.viso.li/api/link/abc123/stats/summary?startDate=2026-10-12T00:00:00.000Z&endDate=2026-10-19T00:00:00.000Z" \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/link/abc123/stats/summary?startDate=2026-10-12T00:00:00.000Z&endDate=2026-10-19T00:00:00.000Z", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. `deviceTypes` and `operatingSystems` are null on FREE. `referers.items` is null below PRO. ```json { "slug": "abc123", "startDate": "2026-10-12T00:00:00.000Z", "endDate": "2026-10-19T00:00:00.000Z", "visits": 120, "visitors": 80, "referers": { "unique": 4, "items": [ { "hostname": "google.com", "count": 40 } ] }, "countries": [ { "code": "US", "name": "United States", "count": 70 } ], "deviceTypes": [ { "type": "DESKTOP", "count": 90 } ], "operatingSystems": [ { "os": "macOS", "count": 50 } ] } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | The requested short-link slug. | | startDate | string | yes | ISO 8601 start of the resolved range. | | endDate | string | yes | ISO 8601 end of the resolved range. | | visits | number | yes | Total clicks in the range. | | visitors | number | yes | Unique visitors in the range. | | referers.unique | number | yes | Number of distinct referring hostnames. | | referers.items | object[] \| null | yes | Hostname counts. Null below PRO. | | referers.items[].hostname | string | yes | Referring hostname. | | referers.items[].count | number | yes | Clicks from this hostname. | | countries | object[] | yes | Clicks by country. | | countries[].code | string | yes | ISO country code. | | countries[].name | string | yes | Country name. | | countries[].count | number | yes | Clicks from this country. | | deviceTypes | object[] \| null | yes | Clicks by device type. Null on FREE. Included on STARTER, PRO, and ENTERPRISE. | | deviceTypes[].type | string | yes | MOBILE, TABLET, DESKTOP, BOT, or UNKNOWN. | | deviceTypes[].count | number | yes | Clicks from this device type. | | operatingSystems | object[] \| null | yes | Clicks by operating system. Null on FREE. Included on STARTER, PRO, and ENTERPRISE. | | operatingSystems[].os | string | yes | Operating system name. | | operatingSystems[].count | number | yes | Clicks from this operating system. | ## Errors | Status | Error | Description | | --- | --- | --- | | 404 | Not Found | The slug does not exist or does not belong to you. | | 400 | Bad Request | startDate or endDate is not a valid ISO 8601 datetime, or endDate is before startDate. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Create Link `POST /api/link/create` Creates a short link from a destination URL. The response is the public URL you can share, plus the optional custom domain and alias. Omit `domain` to use viso.li. Custom slugs require a verified custom domain. ## Request body Send a JSON object. Extra keys are ignored by this endpoint. | Field | Type | Required | Description | | --- | --- | --- | --- | | url | string | yes | Destination URL to shorten. Must use http or https. Max 500 characters. | | expiredAt | string | no | Optional ISO 8601 date when the link should stop redirecting. | | domain | string | no | Optional custom hostname you have verified in Settings. Example: go.example.com. | | alias | string | no | Optional custom back-half, 3–64 characters. Letters, numbers, hyphens, and underscores only. Requires domain. | ## Example request This creates a viso.li short link that expires at the end of 2025. ```sh curl -X POST https://www.viso.li/api/link/create \ -H "Authorization: Bearer sk_api_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://mycoolwebsite.com/my-very-long-url", "expiredAt": "2025-12-31T23:59:59.000Z" }' ``` ```js fetch("https://www.viso.li/api/link/create", { method: "POST", headers: { "Authorization": "Bearer sk_api_your_api_key", "Content-Type": "application/json" }, body: JSON.stringify({ "url": "https://mycoolwebsite.com/my-very-long-url", "expiredAt": "2025-12-31T23:59:59.000Z" }) }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `201 Created`. `url` is the full short URL. ```json { "url": "https://viso.li/abc123", "domain": null, "alias": null } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | url | string | yes | Full public short URL. | | domain | string \| null | yes | Custom hostname, or null for viso.li. | | alias | string \| null | yes | Custom back-half when one was set. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | Invalid URL, domain, or alias. Custom slugs need a verified domain. | | 409 | Conflict | That alias is already taken on the chosen domain. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Delete Link `DELETE /api/link/:slug` Deletes one short link you own. The slug stops redirecting. This does not remove a campaign that was using the link. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | slug | string | yes | Public slug of the shortened link. | ## Example request ```sh curl -X DELETE https://www.viso.li/api/link/abc123 \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/link/abc123", { method: "DELETE", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `204 No Content` with an empty body. ## Errors | Status | Error | Description | | --- | --- | --- | | 404 | Not Found | The slug does not exist or does not belong to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Domains `GET /api/domain` Returns custom domains that are active and can be passed as `domain` when creating a link. Domains that are still waiting on DNS, SSL, or a failed check are omitted. Adding and removing domains stays in the dashboard. ## Example request ```sh curl https://www.viso.li/api/domain \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/domain", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. An account with no active domains returns an empty array. ```json { "domains": [ { "name": "go.example.com" } ] } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | domains | object[] | yes | Active custom domains on this account. | | domains[].name | string | yes | Hostname, such as go.example.com. | ## Errors | Status | Error | Description | | --- | --- | --- | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Campaign `GET /api/campaign/:id` Returns one campaign you own: name, dates, template, typed goal config, current progress, and the attached links. Use this when you need the full campaign. For progress only, see GET campaign status. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | The campaign UUID. | ## Example request ```sh curl https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11 \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. ```json { "id": "3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", "name": "Bio link", "description": null, "status": "ACTIVE", "templateKey": "bio-evergreen", "goal": { "type": "CLICKS", "config": { "target": 10000, "windowDays": 30 } }, "progress": { "type": "CLICKS", "target": 10000, "current": 1240, "remaining": 8760, "percent": 12, "reached": false }, "linkCount": 1, "links": [ { "slug": "abc123", "alias": null, "url": "https://example.com/bio" } ], "startsAt": null, "endsAt": null, "createdAt": "2026-08-22T14:00:00.000Z" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | Campaign UUID. | | name | string | yes | Campaign name. | | description | string \| null | yes | Short description, or null. | | status | string | yes | ACTIVE, DRAFT, or ENDED. | | templateKey | string \| null | yes | Stored template key, or null. | | goal | object \| null | yes | Typed goal settings stored for this campaign. | | progress | object \| null | yes | Current vs target, when the goal is numeric. | | linkCount | number | yes | Number of attached links. | | links | object[] | yes | Attached short links with slug, alias, and destination URL. | | links[].slug | string | yes | Public slug of the attached link. | | links[].alias | string \| null | yes | Custom back-half, or null. | | links[].url | string | yes | Destination URL. | | startsAt | string \| null | yes | ISO 8601 start, or null. | | endsAt | string \| null | yes | ISO 8601 end, or null. | | createdAt | string | yes | ISO 8601 time the campaign was created. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The campaign id is not a valid UUID. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 404 | Not Found | No campaign with that id belongs to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # All Campaigns `GET /api/campaign` Returns a paginated list of campaigns you own, including goal config and current progress. Attached links are omitted here. Use GET campaign for the full link set. Filter by createdAt window, stored status, or template key. ## Query parameters All filters are optional. Newest campaigns first. | Field | Type | Required | Description | | --- | --- | --- | --- | | search | string | no | Case-insensitive match on name or description. | | status | string | no | DRAFT, ACTIVE, or ENDED. | | templateKey | string | no | Known template key, for example bio-evergreen. | | createdFrom | string | no | ISO 8601 inclusive start of the createdAt window. | | createdTo | string | no | ISO 8601 inclusive end of the createdAt window. Must be on or after createdFrom. | | page | number | no | Page number, starting at 1. Defaults to 1. | | pageSize | number | no | Items per page, max 100. Defaults to 20. | ## Example request This lists active campaigns created in 2026 through 23 August. ```sh curl "https://www.viso.li/api/campaign?createdFrom=2026-01-01T00:00:00.000Z&createdTo=2026-08-23T23:59:59.000Z&status=ACTIVE&page=1&pageSize=20" \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/campaign?createdFrom=2026-01-01T00:00:00.000Z&createdTo=2026-08-23T23:59:59.000Z&status=ACTIVE&page=1&pageSize=20", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. ```json { "campaigns": [ { "id": "3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", "name": "Bio link", "description": null, "status": "ACTIVE", "templateKey": "bio-evergreen", "goal": { "type": "CLICKS", "config": { "target": 10000, "windowDays": 30 } }, "progress": { "type": "CLICKS", "target": 10000, "current": 1240, "remaining": 8760, "percent": 12, "reached": false }, "linkCount": 1, "startsAt": null, "endsAt": null, "createdAt": "2026-08-22T14:00:00.000Z" } ], "page": 1, "pageSize": 20, "total": 1, "totalPages": 1 } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | campaigns | object[] | yes | Page of campaigns. Same fields as GET campaign, without links. | | campaigns[].id | string | yes | Campaign UUID. | | campaigns[].name | string | yes | Campaign name. | | campaigns[].description | string \| null | yes | Short description, or null. | | campaigns[].status | string | yes | ACTIVE, DRAFT, or ENDED. | | campaigns[].templateKey | string \| null | yes | Stored template key, or null. | | campaigns[].goal | object \| null | yes | Typed goal settings stored for this campaign. | | campaigns[].progress | object \| null | yes | Current vs target, when the goal is numeric. | | campaigns[].linkCount | number | yes | Number of attached links. | | campaigns[].startsAt | string \| null | yes | ISO 8601 start, or null. | | campaigns[].endsAt | string \| null | yes | ISO 8601 end, or null. | | campaigns[].createdAt | string | yes | ISO 8601 time the campaign was created. | | page | number | yes | Current page. | | pageSize | number | yes | Requested page size. | | total | number | yes | Total matching campaigns. | | totalPages | number | yes | Total pages at this page size. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | Invalid page, dates, status, or template key. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Create Campaign `POST /api/campaign/create` Groups existing short links around a goal so you can track clicks, unique visitors, or compare channels. Campaigns are available on paid plans and always start as `ACTIVE`. Send a name, a goal, and at least one link slug you own. Use `templateKey` to apply a known template. The API stores that key plus a typed `goal.config` — for example `windowDays` on a bio campaign. ## Request body The body must be JSON. Unknown top-level keys are ignored. Dates use ISO 8601. | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | yes | Campaign name. 1–100 characters. | | description | object | yes | Optional short description. Max 500 characters. | | templateKey | object | no | One of blank, general-clicks, channel-test, content-drop, bio-evergreen, qr-offline, or launch. Omit it to skip template defaults. | | startsAt | string | no | ISO 8601 start date. Required for launch. | | endsAt | string | no | ISO 8601 end date. Required for launch. Not allowed for bio-evergreen. | | goalType | string | yes | One of CLICKS, UNIQUE_VISITORS, or COMPARE. Must match the template when that template locks the goal type. | | goalTarget | number | no | Target value, at least 1. Required for CLICKS and UNIQUE_VISITORS unless goalConfig.target is set. Not used for COMPARE. | | goalConfig | object | no | Typed goal settings. Shape depends on goalType. Unknown keys inside goalConfig are rejected. | | linkSlugs | string[] | yes | Slugs of links you own. At least one. | ## Goal config If you send `goalConfig`, it must match `goalType`. If you omit it and send a known template, the API fills in the template default. | Field | Type | Required | Description | | --- | --- | --- | --- | | target | number | yes | Goal value, at least 1. Used by CLICKS and UNIQUE_VISITORS. | | windowDays | number | no | Rolling click window, 1–365. CLICKS only. Bio template default is 30. | | burstDays | number | no | Burst window, 1–90. UNIQUE_VISITORS only. Content-drop default is 7. | | metric | string | yes | CLICKS or UNIQUE_VISITORS. COMPARE only. Channel-test default is CLICKS. | ## Example request This creates a bio campaign with a 10,000 click goal. Because `templateKey` is `bio-evergreen` and `goalConfig` is omitted, the stored config includes `windowDays: 30`. ```sh curl -X POST https://www.viso.li/api/campaign/create \ -H "Authorization: Bearer sk_api_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "Bio link", "templateKey": "bio-evergreen", "goalType": "CLICKS", "goalTarget": 10000, "linkSlugs": [ "abc123" ] }' ``` ```js fetch("https://www.viso.li/api/campaign/create", { method: "POST", headers: { "Authorization": "Bearer sk_api_your_api_key", "Content-Type": "application/json" }, body: JSON.stringify({ "name": "Bio link", "templateKey": "bio-evergreen", "goalType": "CLICKS", "goalTarget": 10000, "linkSlugs": [ "abc123" ] }) }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `201 Created` with the new campaign, including `templateKey`, typed `goal.config`, attached links, and current progress. ```json { "id": "3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", "name": "Bio link", "description": null, "status": "ACTIVE", "templateKey": "bio-evergreen", "goal": { "type": "CLICKS", "config": { "target": 10000, "windowDays": 30 } }, "progress": { "type": "CLICKS", "target": 10000, "current": 0, "remaining": 10000, "percent": 0, "reached": false }, "linkCount": 1, "links": [ { "slug": "abc123", "alias": null, "url": "https://example.com/bio" } ], "startsAt": null, "endsAt": null, "createdAt": "2026-08-22T14:00:00.000Z" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | Campaign UUID. | | name | string | yes | Campaign name. | | description | string \| null | yes | Short description, or null. | | status | string | yes | ACTIVE, DRAFT, or ENDED. | | templateKey | string \| null | yes | Stored template key, or null. | | goal | object \| null | yes | Typed goal settings stored for this campaign. | | progress | object \| null | yes | Current vs target, when the goal is numeric. | | linkCount | number | yes | Number of attached links. | | links | object[] | yes | Attached short links with slug, alias, and destination URL. | | links[].slug | string | yes | Public slug of the attached link. | | links[].alias | string \| null | yes | Custom back-half, or null. | | links[].url | string | yes | Destination URL. | | startsAt | string \| null | yes | ISO 8601 start, or null. | | endsAt | string \| null | yes | ISO 8601 end, or null. | | createdAt | string | yes | ISO 8601 time the campaign was created. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | Validation failed, a slug is unknown, or a template rule was broken. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Campaign Status `GET /api/campaign/:id/status` Returns how close a campaign is to its goal, plus the stored `templateKey` and typed `goal.config`. Progress uses the campaign date window when dates are set, otherwise all clicks on the attached links. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | The campaign UUID. | ## Example request ```sh curl https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11/status \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11/status", { method: "GET", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. `goal.config` is typed by `goal.type` and keeps template fields such as `windowDays` or `burstDays`. ```json { "id": "3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", "name": "Bio link", "status": "ACTIVE", "templateKey": "bio-evergreen", "goal": { "type": "CLICKS", "config": { "target": 10000, "windowDays": 30 } }, "progress": { "type": "CLICKS", "target": 10000, "current": 1240, "remaining": 8760, "percent": 12, "reached": false } } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | Campaign UUID. | | name | string | yes | Campaign name. | | status | string | yes | ACTIVE, DRAFT, or ENDED. | | templateKey | string \| null | yes | Stored template key, or null. | | goal | object \| null | yes | | | progress | object \| null | yes | Current vs target, when the goal is numeric. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The campaign id is not a valid UUID. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 404 | Not Found | No campaign with that id belongs to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Delete Campaign `DELETE /api/campaign/:id` Deletes one campaign you own. The short links that were attached stay in your account. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | The campaign UUID. | ## Example request ```sh curl -X DELETE https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11 \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", { method: "DELETE", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `204 No Content` with an empty body. ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The campaign id is not a valid UUID. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 404 | Not Found | No campaign with that id belongs to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Attach Links `POST /api/campaign/:id/links` Adds short links you already own to a campaign. The response is the updated campaign, including its attached links. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | The campaign UUID. | ## Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | linkSlugs | string[] | yes | One or more public slugs to attach. | ## Example request ```sh curl -X POST https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11/links \ -H "Authorization: Bearer sk_api_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "linkSlugs": [ "def456" ] }' ``` ```js fetch("https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11/links", { method: "POST", headers: { "Authorization": "Bearer sk_api_your_api_key", "Content-Type": "application/json" }, body: JSON.stringify({ "linkSlugs": [ "def456" ] }) }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. The body matches GET `/api/campaign/:id`. ```json { "id": "3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", "name": "Bio link", "description": null, "status": "ACTIVE", "templateKey": "bio-evergreen", "goal": { "type": "CLICKS", "config": { "target": 10000, "windowDays": 30 } }, "progress": { "type": "CLICKS", "target": 10000, "current": 1240, "remaining": 8760, "percent": 12, "reached": false }, "linkCount": 2, "links": [ { "slug": "abc123", "alias": null, "url": "https://example.com/bio" } ], "startsAt": null, "endsAt": null, "createdAt": "2026-08-22T14:00:00.000Z" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | Campaign UUID. | | name | string | yes | Campaign name. | | description | string \| null | yes | Short description, or null. | | status | string | yes | ACTIVE, DRAFT, or ENDED. | | templateKey | string \| null | yes | Stored template key, or null. | | goal | object \| null | yes | Typed goal settings stored for this campaign. | | progress | object \| null | yes | Current vs target, when the goal is numeric. | | linkCount | number | yes | Number of attached links. | | links | object[] | yes | Attached short links with slug, alias, and destination URL. | | links[].slug | string | yes | Public slug of the attached link. | | links[].alias | string \| null | yes | Custom back-half, or null. | | links[].url | string | yes | Destination URL. | | startsAt | string \| null | yes | ISO 8601 start, or null. | | endsAt | string \| null | yes | ISO 8601 end, or null. | | createdAt | string | yes | ISO 8601 time the campaign was created. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The campaign id is invalid, linkSlugs is empty, or a slug could not be found. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 404 | Not Found | No campaign with that id belongs to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Detach Link `DELETE /api/campaign/:id/links/:slug` Removes one short link from a campaign. The link itself is kept. A campaign must keep at least one link. ## Path parameters | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | The campaign UUID. | | slug | string | yes | Public slug of the link to remove. | ## Example request ```sh curl -X DELETE https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11/links/abc123 \ -H "Authorization: Bearer sk_api_your_api_key" ``` ```js fetch("https://www.viso.li/api/campaign/3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11/links/abc123", { method: "DELETE", headers: { "Authorization": "Bearer sk_api_your_api_key" } }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. The body matches GET `/api/campaign/:id`. ```json { "id": "3f1c0c2a-7b4e-4d91-9c2a-1b8e6f0d2a11", "name": "Bio link", "description": null, "status": "ACTIVE", "templateKey": "bio-evergreen", "goal": { "type": "CLICKS", "config": { "target": 10000, "windowDays": 30 } }, "progress": { "type": "CLICKS", "target": 10000, "current": 1240, "remaining": 8760, "percent": 12, "reached": false }, "linkCount": 1, "links": [ { "slug": "abc123", "alias": null, "url": "https://example.com/bio" } ], "startsAt": null, "endsAt": null, "createdAt": "2026-08-22T14:00:00.000Z" } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | id | string | yes | Campaign UUID. | | name | string | yes | Campaign name. | | description | string \| null | yes | Short description, or null. | | status | string | yes | ACTIVE, DRAFT, or ENDED. | | templateKey | string \| null | yes | Stored template key, or null. | | goal | object \| null | yes | Typed goal settings stored for this campaign. | | progress | object \| null | yes | Current vs target, when the goal is numeric. | | linkCount | number | yes | Number of attached links. | | links | object[] | yes | Attached short links with slug, alias, and destination URL. | | links[].slug | string | yes | Public slug of the attached link. | | links[].alias | string \| null | yes | Custom back-half, or null. | | links[].url | string | yes | Destination URL. | | startsAt | string \| null | yes | ISO 8601 start, or null. | | endsAt | string \| null | yes | ISO 8601 end, or null. | | createdAt | string | yes | ISO 8601 time the campaign was created. | ## Errors | Status | Error | Description | | --- | --- | --- | | 400 | Bad Request | The campaign id is invalid, the slug could not be found, or this is the campaign's last link. | | 403 | Forbidden | Campaigns are not available on the current plan. | | 404 | Not Found | No campaign with that id belongs to you. | | 401 | Unauthorized | Missing, malformed, or invalid Bearer token. | | 429 | Too Many Requests | More than 10 requests per minute and 50 requests per day. Some responses include a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. | # Plans `GET /api/plans` Returns the public subscription catalog: FREE, STARTER, PRO, and ENTERPRISE, in that order. This is the catalog, not the caller's current subscription. Prices are US cents. A monthly price of 999 is $9.99. ## Example request Prices are US cents. 999 means $9.99. ```sh curl https://www.viso.li/api/plans ``` ```js fetch("https://www.viso.li/api/plans", { method: "GET" }) .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error)); ``` ## Success response Returns `200 OK`. Prices are US cents. ```json { "plans": [ { "type": "FREE", "name": "Free", "description": "Try link shortening and basic analytics", "popular": false, "pricing": { "currency": "usd", "monthly": 0, "yearly": 0 }, "features": [ "No credit card required", "10 links per month", "1 year of history", "Unlimited QR codes", "Basic analytics" ] }, { "type": "STARTER", "name": "Starter", "description": "More links and extended analytics", "popular": false, "pricing": { "currency": "usd", "monthly": 299, "yearly": 2999 }, "features": [ "Everything in Free", "100 links per month", "1 custom domain", "2 year of history", "Extended analytics" ] }, { "type": "PRO", "name": "Pro", "description": "Campaigns with advanced analytics and API", "popular": true, "pricing": { "currency": "usd", "monthly": 999, "yearly": 9999 }, "features": [ "Everything in Starter", "500 links per month", "3 custom domains", "API access", "Advanced analytics" ] }, { "type": "ENTERPRISE", "name": "Enterprise", "description": "High-volume links with long retention", "popular": false, "pricing": { "currency": "usd", "monthly": 4999, "yearly": 49999 }, "features": [ "Everything in Pro", "75,000 links per month", "10 custom domains", "5 year of history", "Priority support" ] } ] } ``` | Field | Type | Required | Description | | --- | --- | --- | --- | | plans | object[] | yes | Catalog plans from FREE through ENTERPRISE. | | plans[].type | string | yes | FREE, STARTER, PRO, or ENTERPRISE. | | plans[].name | string | yes | Display name of the plan. | | plans[].description | string | yes | Short description of the plan. | | plans[].popular | boolean | yes | True for the plan highlighted as most popular. | | plans[].pricing.currency | string | yes | Currency code. Always usd. | | plans[].pricing.monthly | number | yes | Monthly price in US cents. 999 is $9.99. | | plans[].pricing.yearly | number | yes | Yearly price in US cents. | | plans[].features | string[] | yes | Feature bullets shown for this plan. | ## Errors | Status | Error | Description | | --- | --- | --- | | 429 | Too Many Requests | More than 2 requests per minute from this IP. The response includes a Retry-After header. | | 500 | Internal Server Error | Unexpected server error. Retry the request. |