An open-source URL shortener with click analytics, custom back-halves, UTM tracking, geo and app targeting, built on Cloudflare Workers.
Website · Dashboard · Docs · API reference · Report a link
Shortener.dev turns a long URL into a short link like srt1.link/spring-sale and records every click: when it happened, where the visitor came from, and which country, browser and device they used.
You can use the hosted version at shortener.dev or run your own copy on Cloudflare. This repository holds the full source: the API, the dashboard and the marketing site.
- Short links with custom back-halves or random 7-character keys
- Click analytics by date, country, city, referrer, browser, OS and device
- Click reports with the raw log of every click
- Link expiration after a date and time
- UTM builder to append
utm_source,utm_medium,utm_campaignand other parameters - Social preview with a custom title, description and image
- Password-protected links
- iOS and Android targeting to send mobile visitors to your app
- Geo targeting to send visitors from a country to a different URL
- Domain verification by email code or Google Search Console (see Safety)
- Teams with roles, projects and API keys
- REST API with an OpenAPI spec and examples in cURL, JavaScript and Python
- Docs for AI agents at
/llms.txt, and every docs page as markdown by adding.mdto its URL
Short links hide their destination, which makes them a favorite tool for phishing and spam. Shortener.dev has several checks so its links stay trustworthy:
- Each domain is verified before creating a short link. Prove ownership with an email code or Google Search Console. Subdomains are included.
- Popular sites just work. YouTube, LinkedIn, Amazon and other trusted sites are whitelisted and need no verification.
- Abused destinations are blocked. Free hosts, tunnels, other shorteners and risky TLDs like
.zipare denied (list). - Disposable emails are rejected at signup, and signups and password resets are protected by Cloudflare Turnstile and limited per IP address.
- API requests are rate limited to stop bulk abuse.
- Accounts and links that break the rules are suspended, and the links stop redirecting - taking customers back to safety.
- Anyone can report a link.
The result: visitors can trust every link, your brand can't be misused, and the short domain stays off spam blocklists.
Self-hosting? Please keep these checks enabled (assertVerifiedDestinations in domain.service.ts). More: Verify a domain.
The REST API lives at https://api.shortener.dev/v1. Create an API key in the dashboard under Settings > API Keys, then send it as a bearer token.
curl -X POST https://api.shortener.dev/v1/links \
-H "Authorization: Bearer $SHORTENER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/blog/spring-launch",
"key": "spring-launch"
}'Response:
{
"key": "spring-launch",
"url": "https://example.com/blog/spring-launch",
"short_url": "https://srt1.link/spring-launch",
"is_enabled": true,
"created_at": "2030-03-01T10:00:00Z"
}const response = await fetch('https://api.shortener.dev/v1/links', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SHORTENER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/blog/spring-launch',
key: 'spring-launch',
}),
});
if (!response.ok) {
const { message } = await response.json();
throw new Error(message);
}
const link = await response.json();
console.log(link.short_url);import os
import requests
response = requests.post(
"https://api.shortener.dev/v1/links",
headers={"Authorization": f"Bearer {os.environ['SHORTENER_API_KEY']}"},
json={"url": "https://example.com/blog/spring-launch", "key": "spring-launch"},
)
response.raise_for_status()
print(response.json()["short_url"])Every option in the dashboard is a field on the same request:
{
"url": "https://example.com/shop",
"key": "summer-shop",
"expire_at": "2030-08-31T23:59:00Z",
"params": { "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "summer_sale" },
"title": "Summer sale, up to 40% off",
"description": "Our biggest sale of the year ends August 31.",
"image": "https://example.com/images/summer-og.png",
"password": "sunny-days",
"ios": "https://apps.apple.com/us/app/your-app/id000000000",
"android": "https://play.google.com/store/apps/details?id=com.example.app",
"geo": [{ "country": "GB", "url": "https://example.co.uk/shop" }]
}| Method | Endpoint | Description |
|---|---|---|
GET |
/v1/links?search=spring&limit=50 |
List links, newest first |
GET |
/v1/links/{key} |
Get one link |
PUT |
/v1/links/{key} |
Update a link |
DELETE |
/v1/links/{key} |
Delete a link |
POST |
/v1/domains |
Add a domain |
POST |
/v1/domains/{id}/otp |
Email a verification code to the domain |
POST |
/v1/domains/{id}/verify |
Verify the domain with the code |
GET |
/v1/analytics |
Click analytics |
GET |
/v1/reports |
Raw click log |
Errors return a JSON message. Common status codes: 400 invalid request, 401 missing or invalid API key, 402 plan limit reached, 403 destination domain not verified, 429 rate limited.
The full spec is at api.shortener.dev/openapi.json, and the interactive API reference lets you send test requests.
This is a Turborepo monorepo with npm workspaces.
| Path | Description | Runs on |
|---|---|---|
apps/api |
REST API (api.shortener.dev): links, domains, analytics, auth, billing, emails |
Cloudflare Workers + Hono |
apps/app |
Dashboard (app.shortener.dev): React single-page app |
Cloudflare Workers assets |
apps/web |
Marketing site and docs (shortener.dev): TanStack Start with SSR |
Cloudflare Workers |
packages/eslint-config |
Shared ESLint config | |
packages/typescript-config |
Shared tsconfig.json files |
|
packages/ui |
Shared React components |
Each app has its own README with details: API, Dashboard, Website.
- Cloudflare Workers with D1, KV, R2, Queues, Analytics Engine, Rate Limiting and Email Routing
- Hono and
@hono/zod-openapifor the API and its OpenAPI spec - React, Vite, Tailwind CSS and shadcn/ui for the dashboard
- TanStack Start for the website
- React Email for transactional emails
- Stripe for billing
- TypeScript everywhere
- Node.js 24 or later
- A Cloudflare account (the free plan is enough for local development)
Wrangler is installed with the project dependencies.
git clone https://github.com/Agenty/shortener.git
cd shortener
npm installCreate apps/api/.dev.vars. See Environment variables in the API README for the full list. The minimum to sign up and create links locally:
ENVIRONMENT=development
JWT_SECRET=a-long-random-string
APP_URL=http://localhost:5173
API_URL=http://localhost:8787
DOMAIN=localhostcd apps/api
npm run sql:devThis runs schema/query.sql against a local D1 database in .wrangler/.
From the repository root:
npm run dev| App | URL |
|---|---|
| API | http://localhost:8787 |
| Dashboard | http://localhost:5173 |
| Website | http://localhost:3000 |
To run a single app, use a filter, for example npx turbo dev --filter=shortener-api.
Other root scripts:
npm run build # build every app
npm run lint # lint every app
npm run check-types # type-check every app
npm run format # format with PrettierEach app deploys as its own Worker. The wrangler.json files in this repository point to the resources used by shortener.dev, so replace their IDs with your own.
cd apps/api
npx wrangler login
npx wrangler d1 create shortener
npx wrangler kv namespace create KVCACHE
npx wrangler kv namespace create LINKS
npx wrangler r2 bucket create shortener
npx wrangler queues create shortener-queueCopy the D1 database_id and the two KV id values into apps/api/wrangler.json, in both the top-level config and the previews block.
npm run sqlAdd each value from .dev.vars as a secret, with production URLs:
npx wrangler secret put JWT_SECRET
npx wrangler secret put APP_URL
# ...repeat for the rest# API
cd apps/api && npm run deploy
# Dashboard
cd apps/app && npm run build && npx wrangler deploy
# Website
cd apps/web && npm run deployThen add a custom domain to each Worker in the Cloudflare dashboard, and update:
- the API URL in
apps/app/src/env.tsandapps/web/src/lib/site.ts - the short link domain (
SHORT_DOMAIN) inapps/api/services/links.service.tsandapps/app/src/env.ts
Shortener.dev is free to use and free to self-host. Sponsorships pay for maintainer time, security fixes, abuse prevention on the hosted service and the infrastructure behind the free tier.
Funding details for foundations and grant programs are published in funding.json, following the funding.json standard.
Issues and pull requests are welcome.
- Fork the repository and create a branch from
development. - Make your change and run
npm run lintandnpm run check-types. - Open a pull request describing what changed and why.
To report a security issue, email support@shortener.dev instead of opening a public issue.
Shortener.dev is built and maintained by Agenty Analytics Pvt. Ltd. as open core: the core product is fully open source under the AGPLv3(LICENSE), and a commercial license is available for organizations that need more than the AGPLv3 allows.
For a commercial license, the hosted Enterprise plan or self-hosting support, contact us at shortener.dev/enterprise.
Built and hosted at shortener.dev, the free URL shortener with click analytics.
Made by Agenty.