Skip to main content

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

  1. Go to app.bitly.com
  2. Settings → Developer Settings → API
  3. 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,
  };
}
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;
}
// 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

PlanPriceLinks/MonthFeatures
Free$010Basic shortening
Core$8/month250Custom back-halves
Growth$29/month1,500Custom domains, QR codes
Premium$199/month10,000Advanced analytics, API
EnterpriseCustomUnlimitedSSO, dedicated support

Bitly vs Alternatives

FeatureBitlyShort.ioRebrandlyTinyURL
Free tier10 links1,000 links25 linksUnlimited
Custom domains✅ (paid)✅ (free)
APILimited
QR codes
Click analytics
Self-hostable

Common Mistakes

MistakeImpactFix
Not handling duplicate URLsBitly returns existing bitlinkCheck response for existing link
Exceeding rate limits429 errorsImplement backoff, batch with delays
Shortening already-short URLsDouble redirect, slowerCheck if URL is already a bitlink
Not tracking campaign tagsCan't attribute clicks to campaignsUse UTM parameters + Bitly tags
Ignoring 403 on custom back-halvesBack-half already takenHandle error, suggest alternatives

Choosing a URL shortener API? Compare Bitly vs Short.io vs Rebrandly on APIScout — pricing, API features, and custom domain support.

Comments