# URL shortener API: shorten links from code, no key required

> Use the Blinkhop URL shortener API without a key: create zou.sh links with curl, Python or JavaScript, shorten 100 URLs at once, and handle errors and rate limits.

Source: https://blinkhop.com/guides/url-shortener-api

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 at `https://blinkhop.com/api/v1`
- **Format:** JSON in, JSON out. `POST /links` also accepts a form-encoded `url=...` 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](https://blinkhop.com/docs/api) documents every field. If you just want a first link in a minute, the [quickstart](https://blinkhop.com/docs/quickstart) is shorter.

## Create a short link with curl

`POST /links`

```bash
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:

```json
{
  "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:

```bash
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`:

```bash
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:

```python
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:

```js
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](https://blinkhop.com/docs/code-samples).

## Shorten up to 100 URLs with the bulk endpoint

`POST /links/bulk`

The bulk endpoint takes a `urls` array with up to 100 URLs:

```bash
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:

```json
{
  "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](https://blinkhop.com/pricing) 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-Remaining` gets low, and after a 429, wait for `Retry-After` instead of retrying in a tight loop.
- **Store the result.** Save `short_url` next 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_url` so 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:

```bash
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:

```bash
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](https://blinkhop.com/guides/shorten-links-with-ai-mcp).

## Key takeaways

- One POST to `https://api.blinkhop.com/v1/links` returns a zou.sh link, a QR code URL and a preview URL, with no key needed.
- Add `?format=text` for a plain short URL, or an `alias` for a custom ending.
- `POST /links/bulk` shortens 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 for `Retry-After` seconds after a 429.
- The same URL returns the same link, so retries are safe.

## FAQ

### Do I need an API key to use the Blinkhop API?

No. You can call the API without a key, within 20 new links per minute and 300 per day per IP address. API keys with higher limits come with accounts, which are in early access.

### What happens if I shorten the same URL twice?

The API returns the existing link with HTTP 200 instead of creating a new one, which returns HTTP 201. This applies to links without a custom ending.

### Can I call the API from a web page?

Yes. CORS is open, with Access-Control-Allow-Origin set to *, so browser code on any site can call the API directly.

### Is there an OpenAPI specification?

Yes. An OpenAPI 3.1 spec is available at https://api.blinkhop.com/v1/openapi.json, ready to import into API clients and code generators.
