How to Create a URL Shortener with the Bitly API
·APIScout Team
bitlyurl shortenerlinkstutorialapi integration
How to Create a URL Shortener with the Bitly API
Bitly shortens URLs, tracks clicks, and generates QR codes. This guide covers the Bitly API v4 for programmatic link management — shortening, custom domains, analytics, and bulk operations.
What You'll Build
- URL shortening with custom back-halves
- Click analytics and tracking
- QR code generation
- Custom branded domains
- Bulk URL shortening
Prerequisites: Bitly account (free: 10 links/month, Basic: $8/month for 250 links).
1. Setup
Get API Token
- Go to app.bitly.com
- Settings → Developer Settings → API
- Generate Access Token
BITLY_ACCESS_TOKEN=your_access_token
BITLY_GROUP_GUID=your_default_group_guid
API Helper
// lib/bitly.ts
const BITLY_TOKEN = process.env.BITLY_ACCESS_TOKEN!;
const BASE_URL = 'https://api-ssl.bitly.com/v4';
export async function bitlyApi(
path: string,
options?: RequestInit & { params?: Record<string, string> }
) {
let url = `${BASE_URL}${path}`;
if (options?.params) {
const searchParams = new URLSearchParams(options.params);
url += `?${searchParams}`;
}
const res = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${BITLY_TOKEN}`,
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!res.ok) {
const error = await res.json();
throw new Error(`Bitly API error: ${error.message || res.statusText}`);
}
return res.json();
}
2. Shorten URLs
Basic Shortening
export async function shortenUrl(longUrl: string, options?: {
title?: string;
tags?: string[];
customBackhalf?: string;
domain?: string;
}): Promise<{
shortUrl: string;
id: string;
longUrl: string;
}> {
const data = await bitlyApi('/shorten', {
method: 'POST',
body: JSON.stringify({
long_url: longUrl,
domain: options?.domain || 'bit.ly',
...(options?.title && { title: options.title }),
...(options?.tags && { tags: options.tags }),
}),
});
// Set custom back-half if provided
if (options?.customBackhalf) {
await bitlyApi(`/bitlinks/${data.id}`, {
method: 'PATCH',
body: JSON.stringify({
custom_bitlinks: [`${options.domain || 'bit.ly'}/${options.customBackhalf}`],
}),
});
}
return {
shortUrl: data.link,
id: data.id,
longUrl: data.long_url,
};
}
Create Bitlink with Full Options
export async function createBitlink(options: {
longUrl: string;
title?: string;
tags?: string[];
domain?: string;
deeplinks?: {
appUriPath: string;
installUrl: string;
installType: string;
}[];
}) {
return bitlyApi('/bitlinks', {
method: 'POST',
body: JSON.stringify({
long_url: options.longUrl,
domain: options.domain || 'bit.ly',
title: options.title,
tags: options.tags,
deeplinks: options.deeplinks?.map(dl => ({
app_uri_path: dl.appUriPath,
install_url: dl.installUrl,
install_type: dl.installType,
})),
}),
});
}
API Route
// app/api/shorten/route.ts
import { NextResponse } from 'next/server';
import { shortenUrl } from '@/lib/bitly';
export async function POST(req: Request) {
const { url, title, tags } = await req.json();
if (!url) {
return NextResponse.json({ error: 'URL required' }, { status: 400 });
}
try {
const result = await shortenUrl(url, { title, tags });
return NextResponse.json(result);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
3. Click Analytics
Get Click Summary
export async function getClickSummary(bitlinkId: string, unit: string = 'day', units: number = 30) {
return bitlyApi(`/bitlinks/${bitlinkId}/clicks/summary`, {
params: { unit, units: String(units) },
});
}
// Example response:
// { total_clicks: 1523, unit: 'day', units: 30 }
Get Clicks Over Time
export async function getClicksByTime(
bitlinkId: string,
options: {
unit?: 'minute' | 'hour' | 'day' | 'week' | 'month';
units?: number;
} = {}
) {
const { unit = 'day', units = 30 } = options;
return bitlyApi(`/bitlinks/${bitlinkId}/clicks`, {
params: { unit, units: String(units) },
});
}
// Response:
// {
// link_clicks: [
// { clicks: 45, date: '2026-03-08T00:00:00+0000' },
// { clicks: 32, date: '2026-03-07T00:00:00+0000' },
// ...
// ]
// }
Get Click Details (Country, Referrer, Device)
export async function getClickMetrics(bitlinkId: string) {
const [countries, referrers, devices] = await Promise.all([
bitlyApi(`/bitlinks/${bitlinkId}/countries`, {
params: { unit: 'day', units: '30' },
}),
bitlyApi(`/bitlinks/${bitlinkId}/referrers`, {
params: { unit: 'day', units: '30' },
}),
bitlyApi(`/bitlinks/${bitlinkId}/clicks`, {
params: { unit: 'day', units: '30' },
}),
]);
return { countries, referrers, clicks: devices };
}
4. QR Codes
export async function getQRCode(bitlinkId: string, options?: {
imageFormat?: 'png' | 'svg';
color?: string;
backgroundColor?: string;
size?: number;
}): Promise<string> {
// Bitly QR codes are available via the link
const qrUrl = `https://api-ssl.bitly.com/v4/bitlinks/${bitlinkId}/qr`;
const params: Record<string, string> = {};
if (options?.imageFormat) params.image_format = options.imageFormat;
if (options?.color) params.color = options.color;
const res = await fetch(`${qrUrl}?${new URLSearchParams(params)}`, {
headers: {
Authorization: `Bearer ${process.env.BITLY_ACCESS_TOKEN}`,
},
});
// For PNG, return as base64 data URL
if (options?.imageFormat === 'png' || !options?.imageFormat) {
const buffer = await res.arrayBuffer();
return `data:image/png;base64,${Buffer.from(buffer).toString('base64')}`;
}
// For SVG, return SVG string
return res.text();
}
5. Custom Branded Domains
// List available branded domains
export async function getBrandedDomains() {
return bitlyApi('/bsds'); // Branded Short Domains
}
// Use custom domain when shortening
const result = await shortenUrl('https://yourapp.com/signup', {
domain: 'yourbrand.link', // Your custom short domain
customBackhalf: 'signup',
});
// Result: https://yourbrand.link/signup
6. Bulk URL Shortening
export async function bulkShorten(urls: {
longUrl: string;
title?: string;
tags?: string[];
}[]): Promise<{
shortUrl: string;
longUrl: string;
id: string;
}[]> {
const results = [];
// Bitly doesn't have a batch endpoint — process sequentially with rate limiting
for (const url of urls) {
try {
const result = await shortenUrl(url.longUrl, {
title: url.title,
tags: url.tags,
});
results.push(result);
} catch (error: any) {
results.push({
shortUrl: '',
longUrl: url.longUrl,
id: '',
error: error.message,
});
}
// Rate limit: 150 requests per minute on free plan
await new Promise(resolve => setTimeout(resolve, 400));
}
return results;
}
7. Link Management
// Update a bitlink
export async function updateBitlink(bitlinkId: string, updates: {
title?: string;
tags?: string[];
archived?: boolean;
}) {
return bitlyApi(`/bitlinks/${bitlinkId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
}
// Get link info
export async function getBitlink(bitlinkId: string) {
return bitlyApi(`/bitlinks/${bitlinkId}`);
}
// List all bitlinks
export async function listBitlinks(groupGuid: string, options?: {
size?: number;
page?: number;
tags?: string[];
}) {
const params: Record<string, string> = {
size: String(options?.size || 50),
page: String(options?.page || 1),
};
if (options?.tags) {
params.tags = options.tags.join(',');
}
return bitlyApi(`/groups/${groupGuid}/bitlinks`, { params });
}
8. URL Shortener Component
// components/UrlShortener.tsx
'use client';
import { useState } from 'react';
export function UrlShortener() {
const [url, setUrl] = useState('');
const [shortUrl, setShortUrl] = useState('');
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
const handleShorten = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
const res = await fetch('/api/shorten', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
const data = await res.json();
setShortUrl(data.shortUrl);
setLoading(false);
};
const handleCopy = () => {
navigator.clipboard.writeText(shortUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div style={{ maxWidth: 500, margin: '0 auto' }}>
<form onSubmit={handleShorten}>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Paste a long URL..."
required
style={{ width: '100%', padding: '12px', fontSize: '16px' }}
/>
<button
type="submit"
disabled={loading}
style={{ width: '100%', padding: '12px', marginTop: '8px' }}
>
{loading ? 'Shortening...' : 'Shorten URL'}
</button>
</form>
{shortUrl && (
<div style={{
marginTop: '16px',
padding: '12px',
background: '#f0f0f0',
borderRadius: '6px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<a href={shortUrl} target="_blank" rel="noopener noreferrer">
{shortUrl}
</a>
<button onClick={handleCopy}>
{copied ? '✓ Copied' : 'Copy'}
</button>
</div>
)}
</div>
);
}
Pricing
| Plan | Price | Links/Month | Features |
|---|---|---|---|
| Free | $0 | 10 | Basic shortening |
| Core | $8/month | 250 | Custom back-halves |
| Growth | $29/month | 1,500 | Custom domains, QR codes |
| Premium | $199/month | 10,000 | Advanced analytics, API |
| Enterprise | Custom | Unlimited | SSO, dedicated support |
Bitly vs Alternatives
| Feature | Bitly | Short.io | Rebrandly | TinyURL |
|---|---|---|---|---|
| Free tier | 10 links | 1,000 links | 25 links | Unlimited |
| Custom domains | ✅ (paid) | ✅ (free) | ✅ | ❌ |
| API | ✅ | ✅ | ✅ | Limited |
| QR codes | ✅ | ✅ | ✅ | ❌ |
| Click analytics | ✅ | ✅ | ✅ | ❌ |
| Self-hostable | ❌ | ❌ | ❌ | ❌ |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Not handling duplicate URLs | Bitly returns existing bitlink | Check response for existing link |
| Exceeding rate limits | 429 errors | Implement backoff, batch with delays |
| Shortening already-short URLs | Double redirect, slower | Check if URL is already a bitlink |
| Not tracking campaign tags | Can't attribute clicks to campaigns | Use UTM parameters + Bitly tags |
| Ignoring 403 on custom back-halves | Back-half already taken | Handle error, suggest alternatives |
Choosing a URL shortener API? Compare Bitly vs Short.io vs Rebrandly on APIScout — pricing, API features, and custom domain support.