GuidesUTILITY APIS
Free URL shortener APIs tested from the browser
On 28 October 2026, 8 of 13 URL shortener APIs answered fetch() from a browser, and four of them need no key: is.gd, v.gd, da.gd and spoo.me. TinyURL's keyless endpoint only allows https://tinyurl.com and is.gd's Cloudflare check blocks some networks; the free cors.dev proxy fixes both with no signup. For TinyURL's token API and Dub, cors.dev Pro forwards the POST and the token for $5 a month, with a 7-day trial of 1,000 requests.
Which URL shortener API works from the browser
We tested each API on 28 October 2026 from Node and with fetch() in Chromium, from a cloud machine and from home connections in the US and the Netherlands, then again through the free cors.dev proxy.
| API | Free tier | Key | Browser CORS | Through cors.dev |
|---|---|---|---|---|
| is.gd | 200 links an hour per IP | None | Allow-Origin *, Cloudflare 403 from our cloud machine | Free proxy: 200 |
| v.gd | 200 links an hour per IP | None | Same as is.gd | Free proxy: 200 |
| da.gd | No published limit | None | Allow-Origin * | Not needed |
| spoo.me | 20 a minute without a key | Optional | Allow-Origin * | Not needed, it is a POST |
| TinyURL api-create.php | No published limit | None | Only https://tinyurl.com | Free proxy: 200 |
| TinyURL API | 30 links a month | Bearer token | Only https://tinyurl.com | cors.dev Pro |
| Bitly | 5 links a month | Bearer token | Echoes your origin | Not needed |
| Rebrandly | 10 links a month | apikey header | Allow-Origin * | Not needed |
| Short.io | 1,000 links by API | Public key | Allow-Origin * | Not needed |
| T.LY | No free plan, 5-day trial | Bearer token | Allow-Origin * | Not needed |
| Dub | 25 links a month | Bearer token | None | cors.dev Pro |
| CleanURI | Free | None | Timed out after 30 s | 504 upstream_timeout |
| shrtco.de | Offline | None | Domain no longer resolves | 502 upstream_unreachable |
Eight of the 13 answer a browser directly. Four of those need no key: is.gd, v.gd, da.gd and spoo.me.
is.gd and v.gd: no key, one Cloudflare catch
https://is.gd/create.php?format=json&url= plus your encoded URL returns { "shorturl": "https://is.gd/IeqsUc" }, 38 bytes, with Access-Control-Allow-Origin: *. v.gd works the same way, with the same API and limits.
The catch is Cloudflare. From our cloud machine, both domains answered 403 with a "Just a moment..." challenge page and no CORS header, so Chrome logged "No 'Access-Control-Allow-Origin' header is present on the requested resource" and fetch() threw TypeError: Failed to fetch. The same request through https://proxy.cors.dev/ returned 200. Any visitor on a network Cloudflare challenges gets the same error.
Three more details matter:
- Check the body: with
format=json, a bad URL and a taken custom alias both answered HTTP200, witherrorcode1 or 2 in the JSON. Code 3 means the rate limit and 4 anything else, soresponse.okalone is not enough. - Per-IP limits: 200 new links an hour and 4,800 a day, half that with statistics on, and at most 5 connections at once. Through a proxy, is.gd counts cors.dev's address instead of your visitor's, so call is.gd directly and keep the proxy as the fallback.
- Cache the result: the same long URL came back with the same short link, and is.gd asks you to cache results and shorten on a click, not on page load.
The button below follows all three rules. In Chromium on a home connection, its direct call returned https://is.gd/M9WgvJ. On the cloud machine, the direct call hit the CORS error and the retry through cors.dev returned https://is.gd/jMisGQ. A second click on the same URL made no request.
const shortLinks = new Map();
async function shorten(longUrl) {
if (shortLinks.has(longUrl)) return shortLinks.get(longUrl);
const target = 'https://is.gd/create.php?format=json&url=' + encodeURIComponent(longUrl);
// is.gd sends CORS headers, but Cloudflare challenges some networks, so we retry through cors.dev
let response;
try {
response = await fetch(target);
} catch {
response = await fetch('https://proxy.cors.dev/' + target);
}
if (!response.ok) throw new Error(`is.gd answered ${response.status}`);
// Errors arrive as HTTP 200 with an errorcode, so we check the body too
const data = await response.json();
if (data.errorcode) throw new Error(`is.gd error ${data.errorcode}: ${data.errormessage}`);
shortLinks.set(longUrl, data.shorturl);
return data.shorturl;
}
const input = document.querySelector('#long-url');
const output = document.querySelector('#short-url');
document.querySelector('#shorten').addEventListener('click', async () => {
output.textContent = 'Shortening...';
try {
output.textContent = await shorten(input.value.trim());
} catch (error) {
output.textContent = error.message;
}
});da.gd, spoo.me and TinyURL without a key
- da.gd:
https://da.gd/s?url=returns the short link as plain text with a trailing newline, so call.trim(). It sendsAccess-Control-Allow-Origin: *; a bad URL gets400"Long URL must have http:// or https:// scheme." and a takenshorturlslug gets400"Short URL already taken. Pick a different one." - spoo.me:
POST https://spoo.me/api/v1/shortenwith JSON{"long_url": "..."}answered201withshort_url. It sendsAccess-Control-Allow-Origin: *and exposesX-RateLimit-RemainingandRetry-Afterto your code. Without a key the limit is 20 requests a minute; an API key raises it to 60 a minute and 5,000 a day. The free cors.dev proxy answers this POST with405, and you don't need it. - TinyURL:
https://tinyurl.com/api-create.php?url=returns a 28-byte plain-text link, and a bad URL gets400with the bodyError. It sendsAccess-Control-Allow-Origin: https://tinyurl.com, so Chrome blocks every other origin: "The 'Access-Control-Allow-Origin' header has a value 'https://tinyurl.com' that is not equal to the supplied origin." Puthttps://proxy.cors.dev/in front of the URL and it returns200with the link; the blocked by CORS policy guide explains the message.
Bitly, Rebrandly, Short.io and T.LY send CORS headers
All four answered the browser's preflight, and fake keys came back as readable errors in Chromium: Bitly 403 {"message":"FORBIDDEN"}, Rebrandly and Short.io 401, T.LY 403. You need no proxy, but the key sits in your page.
- Bitly: The free plan allows 5 links a month, with ads. Core costs $10 a month, billed annually, for 100. Over the hourly or per-minute limit you get
429 RATE_LIMIT_EXCEEDED. - Rebrandly: The free plan allows 10 links a month, and the API takes up to 10 requests a second. The key goes in an
apikeyheader. - Short.io: Its public API key is meant for client-side code and works with
POST https://api.short.io/links/public, which creates links. The free plan covers 1,000 links created by API and allows commercial use. The regular/linksendpoint sent noAccess-Control-Allow-Originheader. - T.LY: There is no free plan. Hobby costs $55 a year for 500 links a month, after a 5-day trial with no card.
TinyURL's API and Dub through cors.dev Pro
TinyURL's token API, POST https://api.tinyurl.com/create, answers the preflight with Access-Control-Allow-Origin: https://tinyurl.com, so the browser fails before your token is checked. A free TinyURL account allows 30 links a month. Dub, POST https://api.dub.co/links, sends no CORS header at all; its free plan allows 25 new links a month and 60 API requests a minute.
The free proxy can't carry either: it answers POST with 405 method_not_allowed and refuses Authorization with 400 header_not_allowed. cors.dev Pro forwards both from origins you register:
- Start the trial
Sign in with Google at cors.dev/signup for 7 days and 1,000 requests, no card.
- Register your origin
Add
http://localhost:5173or your app's origin and copy the Connection key. - Send both keys
Put the Connection key in
X-Cors-Keyand your TinyURL token inAuthorization: Bearer, and post tohttps://proxy.cors.dev/https://api.tinyurl.com/create.
const PROXY = 'https://proxy.cors.dev/';
const tinyUrlToken = 'YOUR_TINYURL_API_TOKEN';
const response = await fetch(PROXY + 'https://api.tinyurl.com/create', {
method: 'POST',
headers: {
'X-Cors-Key': 'YOUR_CONNECTION_KEY',
Authorization: `Bearer ${tinyUrlToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS',
domain: 'tinyurl.com',
}),
});
// TinyURL lists its errors in the body, and X-Cors-Source says who answered
const result = await response.json();
if (!response.ok) {
const detail = result.errors?.join(', ') || result.message;
throw new Error(`${response.headers.get('X-Cors-Source')} answered ${response.status}: ${detail}`);
}
console.log(result.data.tiny_url);With a placeholder Connection key, cors.dev answered 401 invalid_key before contacting TinyURL, and the snippet names the source. cors.dev does not hide the TinyURL token: it travels from the browser, where anyone can copy it. Use cors.dev Pro for internal tools and prototypes, and call keyed APIs from your backend in production.
Limits to plan for
- is.gd and v.gd: 200 links an hour and 4,800 a day per IP. After
errorcode3, wait about a minute. - spoo.me: 20 requests a minute without a key; read
X-RateLimit-Remainingbefore you hit it. - Monthly quotas: Bitly 5, Rebrandly 10, Dub 25 and TinyURL 30 links a month on free plans.
- Free cors.dev proxy: GET and HEAD only, about 600 requests a minute per IP, no signup.
- cors.dev Pro: $5 a month for 500,000 requests at 3,000 requests a minute per account and 100 in flight. Both tiers cap responses at 6 MiB with a 30-second upstream deadline.
Good questions.
Which URL shortener API is free without a key?
is.gd, v.gd, da.gd and spoo.me all work with fetch() and no key. is.gd and v.gd allow 200 new links an hour per IP, and spoo.me allows 20 requests a minute. TinyURL's api-create.php needs no key either, but a browser can only reach it through a proxy such as https://proxy.cors.dev/.
Why does TinyURL's API give a CORS error?
Both TinyURL endpoints send Access-Control-Allow-Origin: https://tinyurl.com, so Chrome blocks every other origin. The keyless api-create.php works through the free cors.dev proxy. The token API needs a POST with Authorization, which cors.dev Pro or your own backend can send.
Why does CleanURI fail in the browser?
Many solutions to Frontend Mentor's URL shortening challenge report CORS errors with CleanURI and add a proxy or a backend. On 28 October 2026 every CleanURI request timed out after 30 seconds, home page included. spoo.me's v1 API also shortens with one POST, needs no key and sends Access-Control-Allow-Origin: *.
Is the Bitly API free?
Bitly's free plan allows 5 links a month, and Core costs $10 a month, billed annually, for 100. The API sends CORS headers, so fetch() works from the browser. Anyone who opens DevTools can copy the token and spend those links.
Does cors.dev hide my API token?
No. cors.dev forwards whatever the browser sends, so a token in your JavaScript is visible to every visitor. Use cors.dev Pro for internal tools and prototypes, and keep production tokens on your own server.