# API reference

> Everything the Blinkhop REST API does, with exact request and response formats. JSON in, JSON out, no key needed to start.

Source: https://blinkhop.com/docs/api

## Overview

| Item | Value |
| --- | --- |
| Base URL | `https://api.blinkhop.com/v1` |
| Same API on the main domain | `https://blinkhop.com/api/v1` (same origin as the website, no CORS headers) |
| Formats | JSON in and out. `POST` bodies may also be form-encoded. |
| Authentication | None needed today. See [Authentication](#authentication). |
| OpenAPI 3.1 spec | [`https://api.blinkhop.com/v1/openapi.json`](https://api.blinkhop.com/v1/openapi.json), also at [`/openapi.json`](https://blinkhop.com/openapi.json) |
| Short links | `https://zou.sh/<code>`, see [Short link behavior](#short-link-behavior) |

New to the API? Start with the [quickstart](https://blinkhop.com/docs/quickstart).

### Endpoints at a glance

| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/links` | [Create a short link](#create-a-link) |
| `POST` | `/links/bulk` | [Create up to 100 links at once](#bulk) |
| `GET` | `/links/{code}` | [Look up a link](#look-up-a-link) |
| `GET` | `/expand` | [See where any short link goes](#expand-a-short-link) |
| `POST` | `/reports` | [Report a link](#report-a-link) |
| `GET` | `/status` | [Live status and uptime](#status) |
| `GET` | `/health` | [Health check](#health-check) |

## Authentication

You don't need a key: every endpoint works anonymously, within the [rate limits](#rate-limits). API keys with higher limits will come with accounts, which are in [early access](https://blinkhop.com/pricing#early-access). Keys will use a standard `Authorization: Bearer` header, and anonymous calls will keep working.

## Requests and responses

- Send JSON with `Content-Type: application/json`. `POST /links` also accepts `application/x-www-form-urlencoded`.
- Request bodies are limited to 64 KB. Text is UTF-8.
- Responses are JSON (UTF-8) with `Cache-Control: no-store`, except where noted.
- Timestamps are ISO 8601 in UTC, like `2026-10-11T14:02:11Z`.
- Changes to v1 are additive: new fields may appear in responses, so ignore fields you don't recognize.

## Rate limits

Limits apply per IP address. Each window starts with your first request and resets one minute (or one day) later:

| What | Limit |
| --- | --- |
| Creating links (`POST /links`, `POST /links/bulk`) | 20 per minute and 300 per day. A bulk call uses the whole minute; each of its URLs counts toward the day. |
| Expanding links (`GET /expand`) | 30 per minute and 500 per day |
| Reports (`POST /reports`) | 10 per hour |
| MCP server tools | 120 calls per minute and 5,000 per day |

Rate-limited endpoints return these headers:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed per minute |
| `X-RateLimit-Remaining` | Requests left in the current minute |
| `X-RateLimit-Reset` | When the minute window resets, as a Unix timestamp in seconds |
| `Retry-After` | Only on `429` responses: seconds to wait before retrying |

When you hit a limit, the API answers `429` with the code `rate_limited`. Wait for `Retry-After` seconds, then retry. Don't retry in a tight loop.

## Errors

Every error has the same shape. Branch on `code`: it is stable, while `message` may be reworded.

```json
{
  "error": {
    "code": "alias_taken",
    "message": "That ending is already taken. Try another one.",
    "field": "alias"
  }
}
```

| HTTP | Code | Meaning |
| --- | --- | --- |
| 400 | `missing_url` | No `url` in the request. |
| 400 | `missing_urls` | No `urls` array in a bulk request. |
| 400 | `invalid_json` | The body is not valid JSON. |
| 404 | `not_found` | The link or the endpoint doesn't exist, or the link was disabled. |
| 405 | `method_not_allowed` | Wrong HTTP method, for example `GET /links`. |
| 409 | `alias_taken` | The custom ending is taken or reserved. |
| 413 | `too_large` | The request body is larger than 64 KB. |
| 422 | `invalid_url` | Malformed URL, spaces, scheme other than http or https, embedded credentials, or a private or local address. |
| 422 | `already_short` | The URL is already a zou.sh link. |
| 422 | `url_too_long` | The URL is longer than 4,096 characters. |
| 422 | `unsafe_destination` | The domain is on our phishing and malware blocklist, or the destination was reported and blocked. |
| 422 | `invalid_alias` | The ending breaks the format rules. |
| 422 | `invalid_code` | The value is neither a zou.sh link nor a valid code. |
| 422 | `too_many_urls` | A bulk request has more than 100 URLs. |
| 429 | `rate_limited` | Too many requests. See `Retry-After`. |
| 500 | `internal` | Something went wrong on our side. Retry later and check the [status page](https://blinkhop.com/status). |

## Create a link

POST /v1/links

Creates a short link for a long URL, or returns the existing one.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `url` | body | string, required | The destination, http or https. A missing scheme defaults to `https://`. |
| `alias` | body | string, optional | Custom ending: 3 to 40 letters, numbers, `-` or `_`, starting with a letter or a number. Unique regardless of case. |
| `format` | query | `json` or `text` | `text` returns only the short URL, as plain text. Default `json`. |

```bash
curl https://api.blinkhop.com/v1/links \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/spring", "alias": "spring-sale"}'
```

```json
{
  "id": "lnk_spring-sale",
  "code": "spring-sale",
  "short_url": "https://zou.sh/spring-sale",
  "url": "https://example.com/spring",
  "qr_url": "https://zou.sh/spring-sale/qr",
  "preview_url": "https://zou.sh/spring-sale+",
  "clicks": 0,
  "created_at": "2026-10-11T14:02:11Z"
}
```

- `201 Created`: a new link. The `Location` header holds the short URL.
- `200 OK`: the link already existed. That happens when the same URL was shortened before without an ending, or when you send an ending that already points to the same URL. Retries are safe.
- Random codes are 5 characters from an alphabet without look-alike characters (no `0`/`O`, `1`/`l`/`I`).
- Some endings are reserved, such as `api`, `admin`, `login`, `docs`, `help`, `pricing`, `mcp` and `qr`. They return `409 alias_taken`.
- Possible errors: `missing_url`, `invalid_url`, `already_short`, `url_too_long`, `unsafe_destination`, `invalid_alias`, `alias_taken`, `invalid_json`, `too_large`, `rate_limited`.

<span id="bulk"></span>

## Create links in bulk

POST /v1/links/bulk

Shortens up to 100 URLs in one request. Results come back in the same order as the input. An invalid URL gets an `error` object in its slot instead of failing the whole batch.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `urls` | body | array of strings, required | 1 to 100 URLs. Custom endings aren't supported in bulk. |

```bash
curl https://api.blinkhop.com/v1/links/bulk \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/a", "https://example.org/b", "not a url"]}'
```

```json
{
  "links": [
    {
      "id": "lnk_Tq4mZ",
      "code": "Tq4mZ",
      "short_url": "https://zou.sh/Tq4mZ",
      "url": "https://example.com/a",
      "qr_url": "https://zou.sh/Tq4mZ/qr",
      "preview_url": "https://zou.sh/Tq4mZ+",
      "clicks": 0,
      "created_at": "2026-10-11T14:02:11Z",
      "created": true
    },
    {
      "id": "lnk_8wKhd",
      "code": "8wKhd",
      "short_url": "https://zou.sh/8wKhd",
      "url": "https://example.org/b",
      "qr_url": "https://zou.sh/8wKhd/qr",
      "preview_url": "https://zou.sh/8wKhd+",
      "clicks": 0,
      "created_at": "2026-10-11T14:02:11Z",
      "created": true
    },
    {
      "url": "not a url",
      "error": { "code": "invalid_url", "message": "URLs cannot contain spaces." }
    }
  ]
}
```

- The response is always `200 OK` when the batch is processed. `created` is `false` when a link already existed.
- Each URL counts toward your daily limit of 300 links, and a bulk call uses up the current minute's allowance of 20, so the next call waits for `X-RateLimit-Reset`. A batch larger than what you have left for the day, or sent when the minute's allowance is partly used, is rejected as a whole with `429`.
- Possible request-level errors: `missing_urls`, `too_many_urls`, `invalid_json`, `too_large`, `rate_limited`.

## Look up a link

GET /v1/links/{code}

Returns public information about a short link: destination, click count and creation date.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `code` | path | string, required | The part after `zou.sh/`. Case doesn't matter. |

```bash
curl https://api.blinkhop.com/v1/links/spring-sale
```

The response has the same fields as [Create a link](#create-a-link). Unknown or disabled links return `404 not_found`.

## Expand a short link

GET /v1/expand?url={short link}

Shows where any short link goes, without visiting the destination in a browser. It works with zou.sh links and with other shorteners. The expander follows up to 10 redirects, never downloads page content, and checks the final domain against our phishing and malware blocklist.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `url` | query | string, required | The short link to expand. A missing scheme defaults to `https://`. |

```bash
curl "https://api.blinkhop.com/v1/expand?url=https://zou.sh/spring-sale"
```

```json
{
  "url": "https://zou.sh/spring-sale",
  "final_url": "https://example.com/spring",
  "hops": [
    { "url": "https://zou.sh/spring-sale", "status": 302 },
    { "url": "https://example.com/spring", "status": 200 }
  ],
  "redirects": 1,
  "reached": true,
  "flagged": false
}
```

| Field | Description |
| --- | --- |
| `final_url` | The last URL in the chain. |
| `hops` | Each step, with its HTTP `status`, or an `error`: `unreachable` (no answer within 5 seconds), `blocked_address` (private network) or `unsupported_port`. |
| `redirects` | Number of redirects followed. |
| `reached` | `true` when the final URL answered with a status below 400. |
| `flagged` | `true` when the final domain is on our blocklist. `false` means it isn't on the list, not that the page is guaranteed safe. |

Looking up a zou.sh link this way never counts as a click. The same tool is available as a page: [link expander](https://blinkhop.com/preview).

## Report a link

POST /v1/reports

Reports a zou.sh link used for phishing, malware, spam or other abuse. We review every report.

| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `short_url` | body | string, required | The zou.sh link, or just its code. |
| `reason` | body | string, optional | What's wrong with it, up to 1,000 characters. |

```bash
curl https://api.blinkhop.com/v1/reports \
  -H "Content-Type: application/json" \
  -d '{"short_url": "https://zou.sh/Tq4mZ", "reason": "Fake bank login page"}'
```

Returns `202 Accepted` with `{"ok": true}`. Unknown links return `404 not_found`. Humans can use the [abuse form](https://blinkhop.com/abuse) instead.

## Status

GET /v1/status

Live status of short links, the API, the MCP server and the blocklist, plus daily uptime for the last 90 days, measured by our own monitor every minute. Cached for 30 seconds. The [status page](https://blinkhop.com/status) shows the same data.

```json
{
  "status": "operational",
  "checked_at": "2026-10-11T16:30:05Z",
  "monitoring_since": "2026-10-11T15:57:00Z",
  "uptime_90d": 100,
  "components": [
    { "id": "redirects", "name": "Short links (zou.sh)", "status": "operational" },
    { "id": "api", "name": "REST API", "status": "operational" },
    { "id": "mcp", "name": "MCP server", "status": "operational" },
    {
      "id": "safety",
      "name": "Phishing & malware blocklist",
      "status": "operational",
      "domains": 391542,
      "updated_at": "2026-10-11T04:17:00Z"
    }
  ],
  "days": [{ "day": "2026-10-11", "uptime": 100 }]
}
```

`status` is `operational` or `degraded`. Days before monitoring started have `"uptime": null`.

## Health check

GET /v1/health

Returns `{"ok": true, "version": "1.0.0"}` when the API is up. Use it for your own monitoring.

## Short link behavior

| URL | Result |
| --- | --- |
| `https://zou.sh/<code>` | `302` redirect to the destination, with `Cache-Control: private, no-cache` and `X-Robots-Tag: noindex`. |
| `https://zou.sh/<code>+` | Public preview page: destination, creation date, clicks, and a form to report the link. |
| `https://zou.sh/<code>/qr` | SVG QR code of the short link (error correction level M). |
| Unknown code | `404` page. |
| Disabled link | `410` page that says the link was disabled. |

Codes are case-insensitive: `zou.sh/7KP2X` and `zou.sh/7kp2x` open the same link. Redirects set no cookies, and visitor IP addresses are never stored. Clicks from bots, link previews (Slack, WhatsApp, Discord and others), scripts and headless browsers aren't counted.

## CORS

`api.blinkhop.com` answers every origin with `Access-Control-Allow-Origin: *` and exposes the `X-RateLimit-*`, `Retry-After` and `Location` headers, so you can call it from any web page. Browsers may cache preflight responses for up to 24 hours. `blinkhop.com/api/v1` is meant for our own website and sends no CORS headers.

## OpenAPI and AI tools

- The OpenAPI 3.1 description lives at [`https://api.blinkhop.com/v1/openapi.json`](https://api.blinkhop.com/v1/openapi.json). Import it into Postman, Insomnia or an SDK generator.
- [`/llms.txt`](https://blinkhop.com/llms.txt) lists our docs in Markdown for AI assistants, and every docs page has a Markdown version.
- To let an assistant call Blinkhop directly, use the [MCP server](https://blinkhop.com/mcp).
