A URL shortener API lets your code turn a long URL into a short link with a single HTTP request. Blinkhop’s URL shortener API is a JSON REST API at https://api.blinkhop.com/v1 that you can call without a key: send a URL, get back a zou.sh link with its QR code and preview page. This guide walks through working examples in curl, Python and JavaScript, then covers bulk shortening, errors, rate limits and safe retries.
The URL shortener API at a glance
- Base URL:
https://api.blinkhop.com/v1, also available athttps://blinkhop.com/api/v1 - Format: JSON in, JSON out.
POST /linksalso accepts a form-encodedurl=...field. - Authentication: none needed to try. API keys come with accounts, which are in early access.
- CORS: open (
Access-Control-Allow-Origin: *), so any web page can call the API. - Specification: OpenAPI 3.1 at
https://api.blinkhop.com/v1/openapi.json
| Method and path | What it does |
|---|---|
POST /links |
Create a short link, with an optional custom ending |
POST /links/bulk |
Shorten up to 100 URLs in one request |
GET /links/{code} |
Get public information about a link: destination, clicks and creation date |
GET /expand?url=... |
Follow any short link’s redirects and return the chain, the final destination and a safety verdict |
POST /reports |
Report an abusive link |
GET /status |
Live status of redirects, the API and the MCP server |
GET /health |
A simple health check that returns {"ok": true} |
The API reference documents every field. If you just want a first link in a minute, the quickstart is shorter.
Create a short link with curl
POST/links
curl -s https://api.blinkhop.com/v1/links \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/docs/getting-started?ref=readme"}'
The response describes the link:
{
"id": "lnk_7Kp2x",
"code": "7Kp2x",
"short_url": "https://zou.sh/7Kp2x",
"url": "https://example.com/docs/getting-started?ref=readme",
"qr_url": "https://zou.sh/7Kp2x/qr",
"preview_url": "https://zou.sh/7Kp2x+",
"clicks": 0,
"created_at": "2026-10-11T14:02:11Z"
}
| Field | Meaning |
|---|---|
short_url |
The link to share |
url |
The destination |
qr_url |
A print-ready SVG QR code for the short link |
preview_url |
The public preview page: the short link followed by + |
clicks |
Total clicks so far |
created_at |
Creation time in UTC |
The status code tells you what happened: 201 means a new link was created, and 200 means the same URL already had a link, which you get back.
Choose a custom ending
Add an alias to pick the ending yourself:
curl -s https://api.blinkhop.com/v1/links \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/docs/getting-started", "alias": "getting-started"}'
The short link becomes https://zou.sh/getting-started. Endings use 3 to 40 letters, numbers, dashes or underscores, start with a letter or a number, and are unique regardless of letter case. Some words, such as api, admin, docs and qr, are reserved. If the ending already belongs to another link, the API answers with HTTP 409 and the error code alias_taken.
Get plain text with ?format=text
In shell scripts, you often want the short URL and nothing else. Add ?format=text:
SHORT=$(curl -s "https://api.blinkhop.com/v1/links?format=text" \
--data-urlencode "url=https://example.com/docs/getting-started")
echo "Read the docs: $SHORT"
The --data-urlencode option sends the URL as a form field and encodes characters such as & and ?, so URLs with query strings arrive intact.
Shorten a link with Python
This example uses the requests library. It retries after a rate limit and turns API errors into exceptions:
import time
import requests
API = "https://api.blinkhop.com/v1"
def shorten(url, alias=None, attempts=3):
payload = {"url": url}
if alias:
payload["alias"] = alias
for _ in range(attempts):
resp = requests.post(f"{API}/links", json=payload, timeout=10)
if resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", "60")))
continue
data = resp.json()
if resp.status_code in (200, 201):
return data["short_url"]
error = data["error"]
raise ValueError(f"{error['code']}: {error['message']}")
raise RuntimeError("Still rate limited after several attempts")
print(shorten("https://example.com/docs/getting-started"))
Retrying is safe here: because the same URL returns the same link, a request repeated after a timeout does not create a duplicate.
Shorten a link with JavaScript
The same call with fetch runs in browsers and in Node.js 18 or later. Because CORS is open, a page on any domain can call the API directly:
const API = "https://api.blinkhop.com/v1";
async function shorten(url, alias) {
const res = await fetch(`${API}/links`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(alias ? { url, alias } : { url }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(`${data.error.code}: ${data.error.message}`);
}
console.log("Remaining in this window:", res.headers.get("X-RateLimit-Remaining"));
return data.short_url;
}
console.log(await shorten("https://example.com/docs/getting-started"));
Top-level await works in ES modules. In a classic script, call shorten(...).then(console.log) instead. More examples are on the code samples page.
Shorten up to 100 URLs with the bulk endpoint
POST/links/bulk
The bulk endpoint takes a urls array with up to 100 URLs:
curl -s https://api.blinkhop.com/v1/links/bulk \
-H "Content-Type: application/json" \
-d '{"urls": [
"https://example.com/pricing",
"https://example.com/changelog",
"not a url"
]}'
The response contains one result per URL: a link for each URL that could be shortened, and an error for each one that could not, like the third entry above. One bad URL does not sink the whole batch.
Each URL in a bulk request counts toward your rate limit. Without a key, that means at most 20 new links per minute and 300 per day, so split large jobs into batches of 20 and check X-RateLimit-Remaining between requests. The exact response schema is in the API reference and the OpenAPI spec.
Errors: one predictable format
Every error uses the same JSON shape, with a machine-readable code, a human-readable message and, when relevant, the field that caused it:
{
"error": {
"code": "invalid_url",
"message": "That does not look like a valid URL.",
"field": "url"
}
}
Branch on code, not on message: messages are written for people and may change.
| Code | Meaning |
|---|---|
missing_url |
The request has no URL |
invalid_url |
The URL is not a valid public web address |
already_short |
The URL is already a zou.sh link |
url_too_long |
The URL is longer than the API accepts |
unsafe_destination |
The destination is on the phishing and malware blocklist |
invalid_alias |
The custom ending breaks the format rules |
alias_taken (409) |
The custom ending already belongs to another link |
rate_limited (429) |
Too many requests; wait before retrying |
not_found (404) |
No link exists with that code |
invalid_json |
The request body is not valid JSON |
too_large |
The request body is too large |
Rate limits and response headers
Without a key, each IP address can create 20 new links per minute and 300 per day. Every response carries rate-limit headers, so your code never has to guess:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The number of requests allowed in the current window |
X-RateLimit-Remaining |
How many are left in the current window |
X-RateLimit-Reset |
When the current window resets |
Retry-After |
On 429 responses only: how many seconds to wait |
Higher limits come with API keys. A free account (early access) includes a key with 1,000 links per day, and paid plans go up to 50,000 or 250,000 API links per month. The pricing page has the details.
Idempotency: same URL, same link
HTTP does not treat POST requests as idempotent, but this endpoint behaves like one for plain links. Shortening a URL that already has a link, without a custom ending, returns that existing link with status 200 instead of creating another one. In practice:
- A request retried after a timeout or a dropped connection cannot create a duplicate.
- A job that runs twice over the same list of URLs returns the same short links.
- The status code tells you whether a link is new (201) or existing (200).
Matching is done on the URL itself, so changing any part of it, such as a UTM value, produces a different link. For requests with a custom ending, handle alias_taken explicitly.
Best practices for production
- Set a timeout on every request, as in the Python example, and retry on network errors. Retries are safe because the same URL returns the same link.
- Respect the headers. Slow down when
X-RateLimit-Remaininggets low, and after a 429, wait forRetry-Afterinstead of retrying in a tight loop. - Store the result. Save
short_urlnext to the destination in your own database, so you do not call the API on every page view. - Never shorten secrets. Anyone with a zou.sh link can see its destination on the preview page, so keep access tokens, password reset links and private documents out of short links.
- Show people where links go. If your app shortens links for others, display the
preview_urlso readers can check a destination before they click.
Other endpoints worth knowing
GET/links/{code}
Look up a link’s destination, clicks and creation date:
curl -s https://api.blinkhop.com/v1/links/7Kp2x
GET/expand?url=...
Check where any short link leads, including bit.ly, tinyurl.com and t.co links. The API follows up to 10 redirects and returns the chain, the final destination and a safety verdict:
curl -s -G https://api.blinkhop.com/v1/expand --data-urlencode "url=https://bit.ly/XXXXXXX"
Report an abusive zou.sh link with POST /reports and a JSON body such as {"short_url": "https://zou.sh/7Kp2x", "reason": "Phishing page"}. To monitor the service, poll GET /status or GET /health. If your users work in AI assistants rather than scripts, see the guide to shortening links with an MCP server.
Key takeaways
- One POST to
https://api.blinkhop.com/v1/linksreturns a zou.sh link, a QR code URL and a preview URL, with no key needed. - Add
?format=textfor a plain short URL, or analiasfor a custom ending. POST /links/bulkshortens up to 100 URLs per request, with one result per URL.- Errors share one JSON format: branch on
error.code. - Watch
X-RateLimit-Remaining, and wait forRetry-Afterseconds after a 429. - The same URL returns the same link, so retries are safe.