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
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
/api/ai/check-linksScan 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 —
⌘Kon Mac,Ctrl+Kon 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/insightswith date range filtering and side-by-side audience + posting-time views
/api/v1/analytics/insightsReturns 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
/api/v1/analytics/audienceReturns 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
/api/v1/analytics/posting-timesReturns 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-associationfor 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
/api/domains/:id/verifyCheck DNS TXT record and Cloudflare hostname + SSL status. Returns severity (success/warning/error) and canRevalidate flag for stuck/timed-out domains.
/api/domains/:id/revalidateRe-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.tsandplans.ts, fixing the Vercel build type error where"enterprise"was missing fromPlanKey - 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))
}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.Links API
List Links
/api/v2/linksGet paginated, searchable, sortable list of links for the authenticated workspace.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| offset | integer | 0 | Number of results to skip |
| limit | integer | 50 | Max results per page (max 100) |
| search | string | — | Filter by slug, title, or destination URL |
| sortBy | string | createdAt | Field to sort by: createdAt, slug, totalClicks |
| sortOrder | string | desc | asc or desc |
Response
{
"data": [
{
"id": "uuid",
"slug": "my-slug",
"destination": "https://example.com",
"title": "My Link",
"description": "Campaign page",
"tags": ["marketing"],
"totalClicks": 42,
"uniqueClicks": 35,
"isActive": true,
"utmSource": null,
"utmMedium": null,
"utmCampaign": null,
"password": null,
"expiresAt": null,
"clickLimit": null,
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"meta": { "total": 100, "offset": 0, "limit": 50 }
}Create a Link
/api/v2/linksCreate a new short link (secret key required).
Body (JSON):
// Required "destination": "https://example.com/long-url", // Optional — slug auto-generated if omitted "slug": "custom-slug", // Metadata "title": "My Link", "description": "Campaign landing page", "tags": ["marketing", "launch"], // Security "password": "secret123", "expiresAt": "2026-12-31T23:59:59Z", "clickLimit": 1000, // UTM tracking "utmSource": "newsletter", "utmMedium": "email", "utmCampaign": "spring-launch", "utmTerm": "keywords", "utmContent": "hero-banner", // Social preview (OG tags) "ogTitle": "Open Graph Title", "ogDescription": "OG description", "ogImage": "https://example.com/og.png", // Deep linking "iosDestination": "https://apps.apple.com/...", "androidDestination": "https://play.google.com/...", // A/B testing "abTestEnabled": false
Example — create a link with password + UTM
curl -X POST "https://pivoturl.vercel.app/api/v2/links" \
-H "Authorization: Bearer lf_sk_your-secret-key" \
-H "Content-Type: application/json" \
-d '{
"destination": "https://example.com/black-friday",
"slug": "bf-2026",
"title": "Black Friday 2026",
"password": "secret123",
"utmSource": "email",
"utmMedium": "newsletter",
"utmCampaign": "black-friday-2026"
}'Get a Link
/api/v2/links/:idGet a single link by ID.
curl -H "Authorization: Bearer lf_sk_..." \ "https://pivoturl.vercel.app/api/v2/links/link-id"
Update a Link
/api/v2/links/:idUpdate link fields (secret key required).
Send only the fields you want to update:
curl -X PATCH "https://pivoturl.vercel.app/api/v2/links/link-id" \
-H "Authorization: Bearer lf_sk_your-secret-key" \
-H "Content-Type: application/json" \
-d '{ "title": "Updated Title", "isActive": true }'Delete a Link
/api/v2/links/:idDeactivate a link (secret key required).
Links are soft-deleted — isActive is set to false.
curl -X DELETE "https://pivoturl.vercel.app/api/v2/links/link-id" \ -H "Authorization: Bearer lf_sk_your-secret-key"
Analytics API
Overview
/api/v2/analytics/overviewGet aggregate analytics for the workspace or a single link.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| range | string | 30d | 7d, 30d, 90d, or custom |
| from | ISO date | — | Start date (required if range=custom) |
| to | ISO date | — | End date (required if range=custom) |
| linkId | uuid | — | Filter 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
/api/v2/analytics/breakdownGet click breakdown by dimension (country, device, browser, OS, referrer).
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| dimension | string | country | country, device, browser, os, or referrer |
| range | string | 7d | 7d, 30d, 90d, or custom |
| linkId | uuid | — | Filter 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
/api/v2/analytics/timeseriesGet click volume over time (daily or hourly).
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| groupBy | string | day | day or hour |
| range | string | 7d | 7d, 30d, 90d, or custom |
| linkId | uuid | — | Filter 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
/api/v2/analytics/top-linksGet top-performing links with 7-day trend data.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| range | string | 7d | 7d, 30d, 90d, or custom |
| limit | integer | 10 | Max 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"
}
]
}Link Checker
The Link Checker scans your workspace links for HTTP health (broken URLs, content drift) and enriches results with Cloudflare URL Scanner safety data. All Cloudflare data is pulled from the database — no extra API calls are made during the check.
Check Links
/api/ai/check-linksScan workspace links for HTTP status, content drift (AI-powered), and return Cloudflare safety enrichment.
Body (JSON):
{
"workspaceId": "ws_uuid", // Required
"linkIds": ["link-1", "link-2"] // Optional — omit to scan all (max 50)
}Response
{
"checked": 12,
"broken": 2,
"changed": 1,
"results": [
{
"linkId": "uuid",
"slug": "my-link",
"destination": "https://example.com",
"status": "ok", // "ok" | "broken" | "changed"
"statusCode": 200,
"cloudflare": {
"safetyStatus": "safe", // "unknown" | "pending" | "safe" | "suspicious" | "malicious" | "error"
"safetyTrustScore": 92,
"safetyTrustBand": "high", // "unknown" | "low" | "medium" | "high" | "verified"
"safetyScannedAt": "2026-05-28T10:00:00.000Z",
"safetyVerdict": {
"malicious": false,
"categories": ["Technology", "SaaS"],
"domain": "example.com",
"country": "US",
"technologies": [{ "name": "Next.js", "categories": ["JavaScript frameworks"] }]
},
"redirectChain": [
{ "url": "https://example.com", "status": 301 },
{ "url": "https://www.example.com", "status": 200 }
],
"performance": { "ttfbMs": 120, "fcpMs": 450, "loadMs": 1200 },
"pageIp": "104.21.32.1",
"pageCountry": "US",
"pageServer": "cloudflare",
"radarRank": 1523,
"contactedDomains": ["cdn.example.com", "analytics.example.com"]
}
}
]
}Status Meanings
| Status | Meaning |
|---|---|
| ok | HTTP 2xx/3xx, content unchanged |
| broken | HTTP 4xx/5xx or connection timeout |
| changed | Page title changed significantly (AI-detected content drift) |
Cloudflare Safety Fields
The cloudflare object is populated from the latest finished Cloudflare URL Scanner report stored in the database. If a link has never been scanned, safetyStatus will be "unknown" and other fields will be null.
- safetyTrustScore — 0–100 composite score based on verdict, redirect chain, technologies, and asset risk flags
- safetyTrustBand — human-readable band: low (0–30), medium (31–60), high (61–85), verified (86–100)
- redirectChain — ordered list of HTTP redirects from submitted URL to final URL
- performance — TTFB, First Contentful Paint, and full page load time in milliseconds
- technologies — detected tech stack (frameworks, CMS, analytics, etc.)
- radarRank — Cloudflare Radar popularity rank (1 = most popular globally)
QR Code API
/api/v2/qrGenerate a QR code PNG for any URL.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
| url | string | — | The URL to encode (required) |
| size | integer | 512 | Image size in px (64–2048) |
| fgColor | hex | #000000 | Foreground colour |
| bgColor | hex | #ffffff | Background colour, or transparent |
| errorLevel | string | M | L, 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
/api/v2/workspaceGet 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"
}
}/api/v2/workspaceUpdate 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.
/api/v2/keysList all API keys for the workspace.
/api/v2/keysCreate 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"
}
}/api/v2/keys/:idRevoke (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 Type | Frequency | Description |
|---|---|---|
| link.created | Low | A new short link was created |
| link.updated | Low | A link was updated — changes shows old and new values |
| link.deleted | Low | A link was permanently deleted |
| link.clicked | High | A link received a click — full geo, device, and referrer context |
| conversion.tracked | Medium | A conversion was attributed to a link click — includes attribution model and revenue |
| workspace.member_added | Low | A new member joined the workspace |
| workspace.member_removed | Low | A member was removed from the workspace |
| workspace.plan_changed | Rare | The workspace plan was upgraded or downgraded |
| domain.verified | Rare | A custom domain passed DNS verification |
| domain.deleted | Rare | A custom domain was removed |
| qr.scanned | Medium | A QR code was scanned — fires alongside link.clicked |
| link_gallery.viewed | Medium | A 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 OKimmediately and process asynchronously. - All events carry
workspaceId. Use a single endpoint and filter by workspace in your handler. - Use the
svix-idheader 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
| Section | Route | Description |
|---|---|---|
| Account | /dashboard/settings/account | Personal profile, security, connected accounts + Organization profile/creation |
| Members | /dashboard/settings/members | Team members synced from Clerk, invite links, role management |
| Billing | /dashboard/settings/billing | Current plan, usage meters, upgrade options, billing history |
| Domains | /dashboard/settings/domains | Custom domain management with Cloudflare Custom Hostnames |
| API Keys | /dashboard/settings/api-keys | Create/revoke secret and publishable API keys |
| UTM Templates | /dashboard/settings/utm-templates | Pre-configured UTM parameter templates for link creation |
| Audit Logs | /dashboard/settings/audit-logs | Workspace activity history (create/update/delete events) |
| Webhooks | /dashboard/settings/webhooks | Svix 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
| Code | Status | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or invalid API key |
| FORBIDDEN | 403 | Publishable key used for write operation |
| NOT_FOUND | 404 | Resource not found |
| CONFLICT | 409 | Slug already taken |
| VALIDATION_ERROR | 422 | Invalid request body |
| RATE_LIMITED | 429 | Rate limit exceeded — see Retry-After header |
| FEATURE_NOT_AVAILABLE | 402 | Plan upgrade required for this feature |
| INTERNAL_ERROR | 500 | Something 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.
| Plan | Requests / Hour | Response Headers |
|---|---|---|
| Free | 100 | X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After |
| Starter | 1,000 | |
| Growth | 5,000 | |
| Agency | 20,000 | |
| Business | 50,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}`);X-RateLimit-Remaining approaches 0, back off and retry after the timestamp in X-RateLimit-Reset. Rate limits reset on a rolling hourly window.Need help? support@pivoturl.com