API Documentation

Integrate link shortening, click analytics, and QR code generation into your applications using the PivotUrl REST API and first-party SDKs.

What's New

Latest updates from the May 2026 release.

Settings Consolidation

All workspace configuration is now unified under a single Settings page with a persistent sidebar. No more navigating away to separate pages for billing, domains, or API keys.

  • Account — Clerk UserProfile + OrganizationProfile with hash-based routing (never leaves the settings layout)
  • Members — team management with Clerk organization sync
  • Billing — plan overview, usage meters, upgrade options, billing history (moved from /dashboard/billings)
  • Domains — custom domain management with Cloudflare integration (moved from /dashboard/domain)
  • API Keys — create, revoke, and manage API keys (moved from /dashboard/developers/api-keys)
  • UTM Templates — pre-configured UTM parameter templates
  • Audit Logs — workspace activity history
  • Webhooks — Svix portal for endpoint management

Link Checker + Cloudflare Safety

The Link Checker page now integrates Cloudflare URL Scanner data alongside HTTP health checks. Each scanned link shows its safety verdict, trust score, redirect chain, performance metrics, detected technologies, and domain categories — all pulled from existing scan data without extra API calls.

  • Safety badge — Safe / Suspicious / Malicious / Scanning with trust score inline
  • Trust bar — color-coded 0–100 progress bar per link
  • Expandable Cloudflare panel — server IP, country, Radar rank, TTFB/FCP/Load metrics, categories, technologies, redirect chain, phishing warnings
  • Stats cards — 7-column grid showing Total, OK, Broken, Changed + CF Safe, Suspicious, Malicious counts
POST
/api/ai/check-links

Scan workspace links for HTTP status, content drift, and return Cloudflare safety data from the database.

Command Palette

The search bar in the dashboard header is now functional. Clicking it (or pressing Ctrl+K / ⌘K) opens a command palette with quick actions and navigation shortcuts.

  • Quick Actions — Create new link, Create QR Code, Invite team member
  • Navigation — Jump to Overview, Links, Analytics, Domains, Settings, API Keys
  • Keyboard shortcut⌘K on Mac, Ctrl+K on Windows/Linux
  • Fuzzy search — type to filter actions and pages

Account & Organization Management

The Account page under Settings now shows both your personal profile (Clerk UserProfile) and your organization profile (Clerk OrganizationProfile) on the same page. If you don't have an organization yet, you can create one inline without leaving the settings layout.

  • UserProfile — edit name, email, password, 2FA, connected accounts, active sessions
  • OrganizationProfile — org name, logo, members, invitations, roles, danger zone
  • CreateOrganization — inline org creation form when no org exists
  • Hash routing — all Clerk sub-pages use routing="hash" so the settings sidebar never disappears

Sidebar Cleanup

  • Removed from sidebar — Domains, Billing, API Keys, Webhooks (all moved into Settings)
  • Developers section — now only contains API Docs link
  • Workspace section — simplified to just Settings
  • Webhooks page — removed dev-only toolbar buttons (Dark/Light, Read-only, Page path, Feature flags); kept only Expire Sessions and Open in New Tab

Analytics & Smart Insights

A brand-new insights engine that gives you actionable recommendations based on your link performance data. The analytics dashboard now includes AI-powered insights, audience demographics, and posting-time heatmaps to help you optimize your link strategy.

Smart Insights

  • AI-powered recommendations — the engine detects growth trends, top performers, device share changes, peak engagement hours, and country concentration; returns up to 5 actionable insight cards (opportunity, trend, warning, recommendation)
  • Insight cards — color-coded cards with title, description, metric value, and contextual icon for quick scanning
  • Dedicated insights page — new route under /dashboard/analytics/insights with date range filtering and side-by-side audience + posting-time views
GET
/api/v1/analytics/insights

Returns AI-generated insights comparing current vs prior period: growth, top links, device share, peak hour, country concentration.

Audience Profile

  • Demographics breakdown — top device, browser, OS, country, and referrer displayed as labeled progress bars with percentage share
  • Platform split — visual segmented bar showing Mobile / Desktop / Other distribution
  • Natural-language summary — auto-generated sentence describing the audience composition
GET
/api/v1/analytics/audience

Returns audience profile: top devices, browsers, OS, countries, referrers, and platform split percentages.

Posting Times

  • Hourly heatmap — 24-column compact grid showing click distribution across hours
  • Peak hour detection — highlights the best posting time with a ring indicator and time-of-day icon (morning, afternoon, evening, night)
  • Smart recommendation — suggests optimal time-of-day part for posting new links
GET
/api/v1/analytics/posting-times

Returns 24-hour click distribution, peak hour, runner-up, dead zones, and posting recommendation.

Deep Linking

Links can now open native mobile apps instead of the browser. Support for iOS Universal Links and Android App Links with automatic app association file serving.

  • Universal Links (iOS) — enable per-link with an iOS bundle ID; Apple App Site Association file served automatically at /.well-known/apple-app-site-association
  • App Links (Android) — enable per-link with Android package name; Digital Asset Links file served at /.well-known/assetlinks.json
  • Auto-generated association files — both endpoints query the database for links with deep linking enabled, deduplicate identifiers, and serve valid JSON on every request
  • URI scheme fallback — configurable custom URI scheme per link for apps that register their own protocol handler
  • App store redirect — when the app is not installed, links can redirect to the App Store or Google Play store page
  • Internal association API — authenticated endpoint at /api/internal/app-association for workers and provisioning systems to fetch associations for a specific domain

Domains Management

You can now connect your own domain to brand your short links. The domains management page has been fully redesigned with an inline table view, expandable detail panels, and real-time DNS verification status backed by the Cloudflare Custom Hostnames API.

UI Redesign

  • Table view — domains listed with status badges (Verified / Pending / Error) and relative creation time
  • Expandable rows — click any domain to see full metadata, CNAME and TLS status, and DNS record details
  • Inline add form — add a domain directly in the page without modals or slide-overs
  • DNS setup instructions — copy-ready CNAME and TXT record values with one-click copy buttons
  • Verify Now — checks DNS TXT ownership and Cloudflare hostname + SSL status in one call
  • Re-validate SSL — triggers a new Cloudflare SSL certificate issuance when validation times out
  • Three-dot menu — per-row actions for setup instructions, set as primary, and delete
  • Status banner — contextual banner at the top for important domain-related announcements

API Endpoints

POST
/api/domains/:id/verify

Check DNS TXT record and Cloudflare hostname + SSL status. Returns severity (success/warning/error) and canRevalidate flag for stuck/timed-out domains.

POST
/api/domains/:id/revalidate

Re-trigger Cloudflare SSL certificate validation. Calls revalidate() then syncs the updated status from Cloudflare back to the database.

Cloudflare Custom Hostnames Integration

  • Automatic provisioning — when a domain is added, a Cloudflare Custom Hostname is created with http-01 SSL validation
  • Status sync — hostname status (active/pending/blocked) and SSL status (active/pending_validation/validation_timed_out) are stored in the database and updated on each verify check
  • Revalidation — PATCH endpoint calls Cloudflare's revalidate API, then fetches the updated state immediately and persists it
  • Graceful degradation — domain creation and management work even when Cloudflare credentials are not configured; errors are persisted and displayed in the UI

Billing & Webhooks

Dodo Payments Webhook Fixes

Fixed several data persistence bugs in the billing webhook pipeline that caused incorrect plan mapping and billing cycle values.

  • billingCycle normalization — Dodo sends capitalized Month/Year; now correctly mapped to monthly/annual
  • Plan resolution — removed broken planFromConfiguredPrices() that compared product_id against price IDs (always no match); now uses mapProductToPlan() + guessPlanFromName()
  • Amount precision — fixed double-division bug where /100 was applied in both the webhook and the billing page
  • Enterprise plan — corrected enterprise product ID mapping so enterprise customers get the enterprise plan instead of business
  • Enterprise plan added to PLANS — added full enterprise plan definition with planMap.ts and plans.ts, fixing the Vercel build type error where "enterprise" was missing from PlanKey
  • Free plan domains — removed custom domain limit on the Free plan (unlimited domains)

Webhook Event Types Overhaul

  • Comprehensive event taxonomy — overhauled webhook event types with clear prefixes and granular event names across billing, link, and workspace domains
  • Event filtering — subscribers can now opt into specific event types rather than receiving all events
  • Consistent payload shape — standardized event payloads with version field and uniform metadata envelope

UI / UX

  • Light mode — domains page converted to white backgrounds with black accent buttons and gray borders/text; no slate/indigo/dark palettes remaining on that page
  • Sidebar cleanup — Link in Bio nav item removed; COMING SOON badges removed from Domains nav item
  • Nested button fix — resolved hydration error by converting table row from button to div with role="button" and keyboard handler
  • Dropdown clipping fix — removed overflow-hidden from table container so dropdown menus render unclipped outside table bounds

Overview

PivotUrl exposes a REST API at https://pivoturl.vercel.app/api/v2. All requests must be authenticated with a Bearer token. The API supports two key types: secret keys (lf_sk_...) for full CRUD access, and publishable keys (lf_pk_...) for read-only operations safe to use in browser environments.

Responses are JSON. Errors use a consistent { error: { code, message } } shape. Rate limit information is returned in response headers on every request.

Authentication

Send your API key in the Authorization header:

curl

curl -H "Authorization: Bearer lf_sk_your-api-key" \
  "https://pivoturl.vercel.app/api/v2/links?limit=5"

JavaScript / TypeScript

const res = await fetch("https://pivoturl.vercel.app/api/v2/links?limit=5", {
  headers: { Authorization: "Bearer lf_sk_your-api-key" },
});
const json = await res.json();

Python

import requests

res = requests.get(
    "https://pivoturl.vercel.app/api/v2/links",
    headers={"Authorization": "Bearer lf_sk_your-api-key"},
    params={"limit": 5}
)
data = res.json()

Go

package main

import (
    "fmt"
    "net/http"
    "io"
)

func main() {
    req, _ := http.NewRequest("GET", "https://pivoturl.vercel.app/api/v2/links?limit=5", nil)
    req.Header.Set("Authorization", "Bearer lf_sk_your-api-key")
    
    client := &http.Client{}
    resp, _ := client.Do(req)
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}
Security note: Secret keys (lf_sk_) grant full access to your workspace. Never expose them in client-side code or version control. Use publishable keys (lf_pk_) for browser environments.

Analytics API

Overview

GET
/api/v2/analytics/overview

Get aggregate analytics for the workspace or a single link.

Query Parameters

ParamTypeDefaultDescription
rangestring30d7d, 30d, 90d, or custom
fromISO dateStart date (required if range=custom)
toISO dateEnd date (required if range=custom)
linkIduuidFilter to a single link
curl -H "Authorization: Bearer lf_sk_..." \
  "https://pivoturl.vercel.app/api/v2/analytics/overview?range=30d"
{
  "data": {
    "totalClicks": 15230,
    "uniqueClicks": 8921,
    "clicksToday": 234,
    "clicksGrowth": 12.5,
    "topLink": { "id": "uuid", "slug": "my-link", "clicks": 3400 },
    "topCountry": "United States",
    "topDevice": "mobile"
  }
}

Breakdown

GET
/api/v2/analytics/breakdown

Get click breakdown by dimension (country, device, browser, OS, referrer).

Query Parameters

ParamTypeDefaultDescription
dimensionstringcountrycountry, device, browser, os, or referrer
rangestring7d7d, 30d, 90d, or custom
linkIduuidFilter to a single link
curl -H "Authorization: Bearer lf_sk_..." \
  "https://pivoturl.vercel.app/api/v2/analytics/breakdown?dimension=country&range=7d"
{
  "data": [
    { "label": "United States", "clicks": 5400, "percentage": 35.4 },
    { "label": "India", "clicks": 3200, "percentage": 21.0 },
    { "label": "United Kingdom", "clicks": 2100, "percentage": 13.8 }
  ]
}

Timeseries

GET
/api/v2/analytics/timeseries

Get click volume over time (daily or hourly).

Query Parameters

ParamTypeDefaultDescription
groupBystringdayday or hour
rangestring7d7d, 30d, 90d, or custom
linkIduuidFilter to a single link
curl -H "Authorization: Bearer lf_sk_..." \
  "https://pivoturl.vercel.app/api/v2/analytics/timeseries?groupBy=day&range=30d"
{
  "data": [
    { "date": "2026-05-01", "clicks": 450, "uniqueClicks": 320 },
    { "date": "2026-05-02", "clicks": 520, "uniqueClicks": 380 }
  ]
}

Top Links

GET
/api/v2/analytics/top-links

Get top-performing links with 7-day trend data.

Query Parameters

ParamTypeDefaultDescription
rangestring7d7d, 30d, 90d, or custom
limitinteger10Max results
{
  "data": [
    {
      "id": "uuid",
      "title": "My Link",
      "slug": "my-link",
      "url": "https://example.com",
      "clicks": 3400,
      "uniqueClicks": 2100,
      "ctr": 68.3,
      "trend": [120, 150, 98, 200, 175, 160, 210]
    }
  ]
}

Smart Insights

Smart Insights is an AI-powered analytics layer that goes beyond raw click numbers. It surfaces actionable recommendations, audience profiles, and optimal posting times — automatically generated from your click data.

Available in the dashboard sidebar under Insights, and from the Analytics page hero card.

Best Posting Times

Posting Times analyzes the 24-hour click distribution across all your links to identify when your audience is most active. It extracts the hour of day from every click timestamp and groups them into one-hour buckets.

What you get

  • 24-hour heatmap showing click volume per hour
  • Peak hour identification (your highest-traffic window)
  • Runner-up and dead zone detection
  • Natural language recommendation (e.g., "Your audience peaks at 8 PM — schedule links to go live in the evening")
GET /api/v1/analytics/posting-times?workspaceId=ws_xxx&range=30d

{
  "buckets": [
    { "hour": 0,  "label": "12 AM", "clicks": 42,  "percentage": 1.2 },
    { "hour": 8,  "label": "8 AM",  "clicks": 145, "percentage": 4.1 },
    { "hour": 20, "label": "8 PM",  "clicks": 680, "percentage": 19.2 },
    ...
  ],
  "peak":      { "hour": 20, "label": "8 PM",  "clicks": 680 },
  "runnerUp":  { "hour": 9,  "label": "9 AM",  "clicks": 520 },
  "deadZone":  { "hour": 3,  "label": "3 AM",  "clicks": 8 },
  "recommendation": "Your audience is most active during Evening, with peak engagement at 8 PM. Evening accounts for 42% of all clicks..."
}

Audience Intelligence

Audience Intelligence builds a real-time profile of who your visitors are — what devices they use, which browsers, operating systems, countries, and referrers drive your traffic. It surfaces the top value for each dimension with percentage share.

What you get

  • Top device type (mobile / desktop / tablet) with percentage
  • Top browser and OS
  • Top country and referrer source
  • Mobile vs desktop platform split (visual bar + percentages)
  • Summarized audience profile sentence
GET /api/v1/analytics/audience?workspaceId=ws_xxx&range=30d

{
  "topDevice":   { "label": "mobile",  "percentage": 68 },
  "topBrowser":  { "label": "Chrome",  "percentage": 52 },
  "topOs":       { "label": "iOS",     "percentage": 34 },
  "topCountry":  { "label": "United States", "percentage": 41 },
  "topReferrer": { "label": "twitter.com",    "percentage": 28 },
  "mobileShare": 68,
  "desktopShare": 29,
  "platformSplit": [
    { "platform": "Mobile",  "percentage": 68 },
    { "platform": "Desktop", "percentage": 29 },
    { "platform": "Other",   "percentage": 3 }
  ],
  "summary": "Your audience is primarily Mobile (68% mobile, 29% desktop), using Chrome on iOS. Most traffic comes from United States, driven largely by twitter.com."
}

Actionable Insights

The Insights engine automatically scans your workspace data across multiple dimensions and generates contextual, color-coded cards. Each card has a type — opportunity, trend, warning, or recommendation — with a human-readable title, description, and metric badge.

Insight triggers

  • Growth trend — detects traffic surges (>20% up) or drops (>20% down) vs previous period
  • Star performer — flags links that drive more than double the clicks of the second-best link
  • Mobile-first alert — recommends mobile optimization when mobile share exceeds 80%
  • Best posting time — shows your peak hour with engagement advice
  • Country concentration — alerts when a single country drives more than 50% of traffic
GET /api/v1/analytics/insights?workspaceId=ws_xxx&range=30d

{
  "insights": [
    {
      "type": "opportunity",
      "title": "Best Posting Time",
      "description": "Your audience peaks at 8 PM (680 clicks). Schedule your most important links to go live during evening for maximum engagement.",
      "metric": "8 PM",
      "icon": "clock"
    },
    {
      "type": "trend",
      "title": "Traffic Surge",
      "description": "Your click volume is up 34% compared to the previous period. This is a significant growth spike.",
      "metric": "+34%",
      "icon": "trending-up"
    },
    {
      "type": "recommendation",
      "title": "Mobile-First Audience",
      "description": "68% of your traffic is on mobile. Ensure your landing pages load quickly and are fully responsive.",
      "metric": "68% mobile",
      "icon": "smartphone"
    }
  ]
}

QR Code API

GET
/api/v2/qr

Generate a QR code PNG for any URL.

Query Parameters

ParamTypeDefaultDescription
urlstringThe URL to encode (required)
sizeinteger512Image size in px (64–2048)
fgColorhex#000000Foreground colour
bgColorhex#ffffffBackground colour, or transparent
errorLevelstringML, M, Q, or H (error correction)

Returns a PNG image (image/png). The response is raw binary, not JSON.

# Download as file
curl -H "Authorization: Bearer lf_sk_..." \
  "https://pivoturl.vercel.app/api/v2/qr?url=https://example.com&size=512" \
  --output qr.png
// JavaScript — display in browser
const res = await fetch("https://pivoturl.vercel.app/api/v2/qr?url=https://example.com&size=256", {
  headers: { Authorization: "Bearer lf_sk_..." },
});
const blob = await res.blob();
const imgUrl = URL.createObjectURL(blob);
document.querySelector("#qr").src = imgUrl;

Workspace API

GET
/api/v2/workspace

Get current workspace details (name, slug, plan, limits).

curl -H "Authorization: Bearer lf_sk_..." \
  "https://pivoturl.vercel.app/api/v2/workspace"
{
  "data": {
    "id": "uuid",
    "name": "My Workspace",
    "slug": "my-workspace",
    "plan": "starter",
    "isDefault": true,
    "createdAt": "2026-01-01T00:00:00.000Z"
  }
}
PATCH
/api/v2/workspace

Update workspace name (secret key required).

curl -X PATCH "https://pivoturl.vercel.app/api/v2/workspace" \
  -H "Authorization: Bearer lf_sk_your-secret-key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "New Workspace Name" }'

API Keys

Manage API keys programmatically. These endpoints are authenticated via your Clerk dashboard session, not by API key.

GET
/api/v2/keys

List all API keys for the workspace.

POST
/api/v2/keys

Create a new API key.

Body: { "name": "My Key", "keyType": "secret" }. keyType is secret (default) or publishable.

// Response — plaintextKey is shown once only
{
  "data": {
    "name": "My Key",
    "keyPrefix": "lf_sk_a1b2c3d4...",
    "keyType": "secret",
    "plaintextKey": "lf_sk_a1b2c3d4e5f6789012345678901234567890abcdef"
  }
}
DELETE
/api/v2/keys/:id

Revoke (deactivate) an API key.

Manage your keys in the dashboard: /dashboard/settings/api-keys

SDK & Client Libraries

TypeScript / JavaScript

The pivoturl-sdk package is a first-party TypeScript client. It uses native fetch and works in Node.js 18+, Edge Runtimes, and modern browsers.

npm install pivoturl-sdk

Setup

import { PivotUrlClient } from "pivoturl-sdk";

const client = new PivotUrlClient({
  apiKey: "lf_sk_your-secret-key",
  // baseUrl: "https://pivoturl.vercel.app/api/v2",  // optional — auto-detected
});

Links

// List with pagination & search
const { data: links, meta } = await client.links.list({
  offset: 0,
  limit: 20,
  search: "example",
});

// Get by ID
const link = await client.links.get("link-id");

// Create with full options
const newLink = await client.links.create({
  destination: "https://example.com",
  slug: "my-slug",
  title: "My Link",
  tags: ["marketing"],
  password: "secret123",
  expiresAt: "2026-12-31T23:59:59Z",
  utmSource: "newsletter",
  utmCampaign: "spring-launch",
  ogTitle: "OG Title",
  ogImage: "https://example.com/og.png",
});

// Update specific fields
const updated = await client.links.update("link-id", {
  title: "New Title",
  isActive: true,
});

// Soft-delete
await client.links.delete("link-id");

Analytics

// Workspace overview (last 30 days)
const overview = await client.analytics.overview({ range: "30d" });

// Breakdown by country (last 7 days)
const byCountry = await client.analytics.breakdown({
  dimension: "country",
  range: "7d",
});

// Daily timeseries (last 30 days)
const daily = await client.analytics.timeseries({
  groupBy: "day",
  range: "30d",
});

// Per-link analytics
const linkOverview = await client.analytics.overview({
  range: "30d",
  linkId: "your-link-id",
});

// Top links
const topLinks = await client.analytics.topLinks({
  range: "7d",
  limit: 10,
});

QR Codes

// Get raw PNG bytes (ArrayBuffer)
const buffer = await client.qr.generate({
  url: "https://example.com",
  size: 256,
  fgColor: "#000000",
});

// Get data URL (for <img> tags)
const dataUrl = await client.qr.generateDataURL({
  url: "https://example.com",
  size: 512,
});

Workspace & Keys

// Workspace info
const ws = await client.workspace.get();

// Rename workspace
await client.workspace.patch({ name: "New Name" });

// List API keys
const keys = await client.keys.list();

// Create a new key
const created = await client.keys.create("CI/CD Key", "secret");
console.log("Save this key:", created.plaintextKey);

// Revoke a key
await client.keys.revoke("key-id");

Rate Limits

// Each response includes rate limit headers
const res = await client.links.list();
console.log(res.rateLimit); // { limit, remaining, reset }

// Check key type
client.getKeyType(); // "secret" | "publishable"

Python

The pivoturl Python package provides a typed client for the PivotUrl API.

pip install pivoturl
from pivoturl import PivotUrl

client = PivotUrl(api_key="lf_sk_your-secret-key")

# List links
links = client.links.list(limit=10)

# Create a link
link = client.links.create(
    destination="https://example.com",
    slug="my-slug",
    title="My Link",
    tags=["marketing"],
)

# Get analytics overview
overview = client.analytics.overview(range="30d")

# Generate QR code
with open("qr.png", "wb") as f:
    f.write(client.qr.generate("https://example.com"))

Go

go get github.com/pivoturl/sdk-go
package main

import (
    "context"
    "fmt"
    "github.com/pivoturl/sdk-go"
)

func main() {
    client := PivotUrl.NewClient("lf_sk_your-secret-key")

    // List links
    links, _ := client.Links.List(context.Background(), &PivotUrl.ListParams{Limit: 10})
    for _, l := range links.Data {
        fmt.Printf("%s → %s\n", l.Slug, l.Destination)
    }

    // Create a link
    newLink, _ := client.Links.Create(context.Background(), &PivotUrl.CreateLinkParams{
        Destination: "https://example.com",
        Slug:        "my-slug",
        Title:       "My Link",
    })

    // Analytics
    overview, _ := client.Analytics.Overview(context.Background(), &PivotUrl.AnalyticsParams{
        Range: "30d",
    })
    fmt.Printf("Total clicks: %d\n", overview.TotalClicks)
}

Webhooks

Configure webhook endpoints in your dashboard to receive real-time HTTP POST notifications for link events, clicks, conversions, and more. PivotUrl uses Svix for reliable delivery with automatic retries and idempotency.

Envelope

Every webhook payload follows the same envelope:

{
  "eventType": "link.clicked",       // The event type — always matches the Svix header
  "workspaceId": "ws_uuid",          // The workspace this event belongs to
  "data": { /* event-specific fields */ },
  "actorId": "user_uuid",            // (optional) Who performed the action
  "timestamp": "2026-05-20T12:00:00.000Z"
}

Verify payloads using the svix-id, svix-timestamp, and svix-signature headers.

All Event Types

Event TypeFrequencyDescription
link.createdLowA new short link was created
link.updatedLowA link was updated — changes shows old and new values
link.deletedLowA link was permanently deleted
link.clickedHighA link received a click — full geo, device, and referrer context
conversion.trackedMediumA conversion was attributed to a link click — includes attribution model and revenue
workspace.member_addedLowA new member joined the workspace
workspace.member_removedLowA member was removed from the workspace
workspace.plan_changedRareThe workspace plan was upgraded or downgraded
domain.verifiedRareA custom domain passed DNS verification
domain.deletedRareA custom domain was removed
qr.scannedMediumA QR code was scanned — fires alongside link.clicked
link_gallery.viewedMediumA bio page was viewed by a visitor

Example — link.clicked

{
  "eventType": "link.clicked",
  "workspaceId": "ws_9a8b7c6d-5e4f-3a2b-1c0d-ef1234567890",
  "data": {
    "linkId": "3f4a1b2c-1234-5678-abcd-ef0123456789",
    "slug": "summer-sale",
    "domain": "go.acmecorp.com",
    "country": "IN",
    "city": "Mumbai",
    "region": "Maharashtra",
    "device": "iPhone",
    "deviceType": "ios",
    "browser": "Safari",
    "browserVersion": "17.4.1",
    "os": "iOS",
    "osVersion": "17.4.1",
    "referrer": "https://twitter.com/",
    "referrerDomain": "twitter.com",
    "referrerType": "social",
    "isBot": false,
    "isQrScan": false
  },
  "timestamp": "2026-05-20T12:00:00.000Z"
}

Example — conversion.tracked

{
  "eventType": "conversion.tracked",
  "workspaceId": "ws_9a8b7c6d-5e4f-3a2b-1c0d-ef1234567890",
  "data": {
    "linkId": "3f4a1b2c-1234-5678-abcd-ef0123456789",
    "slug": "summer-sale",
    "conversionId": "conv_7b8c9d0e-1234-5678-abcd-ef0123456789",
    "event": "purchase",
    "value": 149.99,
    "currency": "USD",
    "attributionModel": "last_touch",
    "creditPercentage": 100,
    "creditValue": 149.99,
    "customerEmail": "buyer@example.com"
  },
  "timestamp": "2026-05-20T12:05:00.000Z"
}

Best Practices

  • link.clicked is the only high-frequency event. Respond with 200 OK immediately and process asynchronously.
  • All events carry workspaceId. Use a single endpoint and filter by workspace in your handler.
  • Use the svix-id header for idempotency — Svix guarantees at-least-once delivery.

Manage your webhook endpoints in the dashboard under Settings → Webhooks or via the Svix App Portal.

Settings & Account

All workspace configuration lives under /dashboard/settings with a persistent sidebar. Clicking Settings in the main dashboard sidebar opens the settings panel — the sidebar stays visible while you navigate between sections.

Settings Sidebar Sections

SectionRouteDescription
Account/dashboard/settings/accountPersonal profile, security, connected accounts + Organization profile/creation
Members/dashboard/settings/membersTeam members synced from Clerk, invite links, role management
Billing/dashboard/settings/billingCurrent plan, usage meters, upgrade options, billing history
Domains/dashboard/settings/domainsCustom domain management with Cloudflare Custom Hostnames
API Keys/dashboard/settings/api-keysCreate/revoke secret and publishable API keys
UTM Templates/dashboard/settings/utm-templatesPre-configured UTM parameter templates for link creation
Audit Logs/dashboard/settings/audit-logsWorkspace activity history (create/update/delete events)
Webhooks/dashboard/settings/webhooksSvix webhook portal — manage endpoints, event subscriptions, delivery logs

Account Page

The Account page renders Clerk's UserProfile and OrganizationProfile components inline using routing="hash". This means all Clerk sub-pages (edit profile, change password, manage sessions, invite members, etc.) navigate via URL hash changes — the settings sidebar never disappears.

  • UserProfile — name, email, avatar, password, 2FA, connected accounts (Google, GitHub, etc.), active sessions
  • OrganizationProfile — org name, logo, members list, pending invitations, role management, danger zone (delete org)
  • CreateOrganization — shown inline when no organization exists; after creation, the org profile appears immediately

Command Palette

Press Ctrl+K (Windows/Linux) or ⌘K (Mac) anywhere in the dashboard to open the command palette. You can also click the search bar in the header.

  • Quick Actions — Create new link, Create QR Code, Invite team member
  • Navigation — Jump to any dashboard page by typing its name
  • Fuzzy matching — powered by cmdk for instant filtering

Error Handling

All API errors return a consistent JSON shape:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Must be a valid URL"
  }
}

Error Codes

CodeStatusDescription
UNAUTHORIZED401Missing or invalid API key
FORBIDDEN403Publishable key used for write operation
NOT_FOUND404Resource not found
CONFLICT409Slug already taken
VALIDATION_ERROR422Invalid request body
RATE_LIMITED429Rate limit exceeded — see Retry-After header
FEATURE_NOT_AVAILABLE402Plan upgrade required for this feature
INTERNAL_ERROR500Something went wrong on our end

SDK Error Handling

import {
  PivotUrlError,
  AuthenticationError,
  RateLimitError,
  ValidationError,
  NotFoundError,
  ForbiddenError,
} from "pivoturl-sdk";

try {
  await client.links.create({ destination: "not-a-url" });
} catch (err) {
  if (err instanceof ValidationError) {
    console.error("Validation failed:", err.message, err.details);
  } else if (err instanceof RateLimitError) {
    console.error(`Rate limited. Retry after ${err.resetTime}s`);
  } else if (err instanceof AuthenticationError) {
    console.error("Invalid API key. Check your credentials.");
  } else if (err instanceof PivotUrlError) {
    console.error(`${err.code}: ${err.message}`);
  }
}

Rate Limits

Rate limits are applied per workspace per hour based on your plan. Every response includes rate limit headers so you can monitor your usage programmatically.

PlanRequests / HourResponse Headers
Free100X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
Retry-After
Starter1,000
Growth5,000
Agency20,000
Business50,000
// Read rate limit headers in JavaScript
const res = await fetch("https://pivoturl.vercel.app/api/v2/links?limit=1", {
  headers: { Authorization: "Bearer lf_sk_..." },
});
const remaining = res.headers.get("X-RateLimit-Remaining");
const resetAt = res.headers.get("X-RateLimit-Reset");
console.log(`${remaining} requests remaining, resets at ${resetAt}`);
Tip: When X-RateLimit-Remaining approaches 0, back off and retry after the timestamp in X-RateLimit-Reset. Rate limits reset on a rolling hourly window.