# turl.ca — full documentation


---

---
title: "Developer guide — turl.ca"
source: "https://turl.ca/docs"
---
Developers

# Developer guide

Shorten and manage links from the REST API, an AI agent, or your scripts.

[Quick start](#quickstart) [REST API](#rest) [MCP for AI agents](#mcp) [OAuth & DPoP](#oauth) [Discovery](#discovery)

## Quick start

turl.ca shares your pastebin.ca account — nothing new to sign up for. Mint an API key bound to this site, then shorten URLs from your scripts or an AI agent.

Audience-boundA `link:*` key only works on `turl.ca`; it is rejected everywhere else.

```
curl -sS https://turl.ca/api/v1/links \
  -H 'Authorization: Bearer pbca_live_…' \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/a/very/long/link"}'
```

## REST API

A small JSON surface. Send your key as `Authorization: Bearer <api-key>`. Signed-in cookie sessions also work; API-key callers must hold the matching scope. The full spec is at [/api/v1/openapi.json](/api/v1/openapi.json).

### Scopes

link:read link:create link:delete

| Action | Method & path | Scope |
| --- | --- | --- |
| Shorten a URL | `POST /api/v1/links` | `link:create` |
| Read one of your links | `GET /api/v1/links/:code` | `link:read` |
| Expand any code (public) | `GET /api/v1/expand/:code` | — |
| List your links | `GET /api/v1/account/links` | `link:read` |
| List capture versions | `GET /api/v1/links/:code/captures` | `link:read` |
| Request a new capture | `POST /api/v1/links/:code/captures` | `link:create` |
| Publish a completed capture | `POST /api/v1/links/:code/captures/:capture/publish` | `link:create` |
| Download a versioned visual | `GET /api/v1/links/:code/captures/:capture/visual` | `link:read` |
| Retire one of your links | `DELETE /api/v1/links/:code` | `link:delete` |
| Report abuse | `POST /api/v1/abuse` | — |

Trust tiersOnly clean family links in redirect mode go straight through. Clean high/open links and every citation link show a preview; pending links show a safety-check page. Anonymous browser creates require Turnstile and run a live safety check. Citation mode for arbitrary open-tier destinations still requires a signed-in account. Pointing a turl at another known URL shortener is unsupported and returns `400 {"error":"shortener_destination"}` — submit the original destination instead.

Creation outcomesCreate responses (including live SSE and MCP) report `resolves` as `redirect` or `preview`, explain the assigned tier with `trust_reason`, and include `expires_at`. Owner read/list responses report current `resolves` behavior, including `gone` later in the lifecycle. Use these fields instead of inferring behavior from `trust` or `scan_state`.

Citation capture is stagedCitation mode always remains preview-first. A configured account rollout may also admit an asynchronous source capture; the create response reports that admission separately. Capture history reports queue age, attempts, completion, partial, blocked, and failed states. Exact source HTML is download-only, public documents are forced attachments, and public replay is a newly serialized scriptless derivative on a separately configured content origin. No retention or durability SLA is implied.

Why links are immutableA short code always points at the destination it was minted for — turl never lets a code be re-pointed under the people who already shared or clicked it, so a trusted link can't be swapped for a malicious one later. There is no custom-alias or edit API today; owner-editable links may arrive as a future, explicitly granted capability.

```
curl -sS https://turl.ca/api/v1/expand/abc1234
```

```
curl -sS https://turl.ca/api/v1/account/links \
  -H 'Authorization: Bearer pbca_live_…'
```

## MCP for AI agents

turl.ca runs a Model Context Protocol server at `https://turl.ca/mcp`, so Claude Desktop, Cursor, the MCP Inspector, and other agents can shorten and manage links directly.

### Option A — OAuth (interactive clients)

Point your client at the MCP URL and it discovers the rest: it reads `https://turl.ca/.well-known/oauth-protected-resource/mcp`, registers with pastebin.ca (the authorization server), and runs OAuth 2.1 + PKCE. Add to your Claude Desktop config:

```
{
  "mcpServers": {
    "turl": {
      "url": "https://turl.ca/mcp"
    }
  }
}
```

Config path: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `~/.config/Claude/claude_desktop_config.json` (Linux). Cursor uses the same `mcpServers` shape in `~/.cursor/mcp.json`. Try it live with `npx @modelcontextprotocol/inspector`.

### Option B — API-key bearer (automation)

For headless use, send a minted key directly:

```
{
  "mcpServers": {
    "turl": {
      "url": "https://turl.ca/mcp",
      "headers": { "Authorization": "Bearer pbca_live_…" }
    }
  }
}
```

```
pbca account api-key create \
  --scopes link:read,link:create,link:delete \
  --audience https://turl.ca
```

### Tools

| Tool | Scope | Purpose |
| --- | --- | --- |
| `whoami` | — | Echo account id, scopes, audience, and token kind. |
| `create_link` | `link:create` | Shorten a URL; returns resolution, trust reason, safety state, and expiry. |
| `get_link` | `link:read` | Fetch one link with its current resolution behavior. |
| `list_my_links` | `link:read` | List links with current resolution behavior; cursor-paginated. |
| `delete_link` | `link:delete` | Retire one of the caller's links. |

## OAuth & DPoP

pastebin.ca is the OAuth authorization server; turl.ca is a protected resource and never mints tokens.

- OAuth tokens are **audience-bound** to `https://turl.ca/mcp` (RFC 8707) and honored only on the MCP route.
- If a key is **DPoP-bound** (RFC 9449), every MCP request must carry a matching ES256 proof; replays are rejected. Plain bearer keys work without DPoP.
- Revoke a key or connected app anytime from your pastebin.ca account.

## Discovery

| Document | URL |
| --- | --- |
| OpenAPI 3.1 | [`/api/v1/openapi.json`](/api/v1/openapi.json) |
| Agent card | [`/.well-known/agent.json`](/.well-known/agent.json) |
| MCP protected resource | [`/.well-known/oauth-protected-resource/mcp`](https://turl.ca/.well-known/oauth-protected-resource/mcp) |
| LLM site map | [`/llms.txt`](/llms.txt) |


---

---
title: "Developer guide — turl.ca"
source: "https://turl.ca/docs/mcp"
---
Developers

# Developer guide

Shorten and manage links from the REST API, an AI agent, or your scripts.

[Quick start](#quickstart) [REST API](#rest) [MCP for AI agents](#mcp) [OAuth & DPoP](#oauth) [Discovery](#discovery)

## Quick start

turl.ca shares your pastebin.ca account — nothing new to sign up for. Mint an API key bound to this site, then shorten URLs from your scripts or an AI agent.

Audience-boundA `link:*` key only works on `turl.ca`; it is rejected everywhere else.

```
curl -sS https://turl.ca/api/v1/links \
  -H 'Authorization: Bearer pbca_live_…' \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com/a/very/long/link"}'
```

## REST API

A small JSON surface. Send your key as `Authorization: Bearer <api-key>`. Signed-in cookie sessions also work; API-key callers must hold the matching scope. The full spec is at [/api/v1/openapi.json](/api/v1/openapi.json).

### Scopes

link:read link:create link:delete

| Action | Method & path | Scope |
| --- | --- | --- |
| Shorten a URL | `POST /api/v1/links` | `link:create` |
| Read one of your links | `GET /api/v1/links/:code` | `link:read` |
| Expand any code (public) | `GET /api/v1/expand/:code` | — |
| List your links | `GET /api/v1/account/links` | `link:read` |
| List capture versions | `GET /api/v1/links/:code/captures` | `link:read` |
| Request a new capture | `POST /api/v1/links/:code/captures` | `link:create` |
| Publish a completed capture | `POST /api/v1/links/:code/captures/:capture/publish` | `link:create` |
| Download a versioned visual | `GET /api/v1/links/:code/captures/:capture/visual` | `link:read` |
| Retire one of your links | `DELETE /api/v1/links/:code` | `link:delete` |
| Report abuse | `POST /api/v1/abuse` | — |

Trust tiersOnly clean family links in redirect mode go straight through. Clean high/open links and every citation link show a preview; pending links show a safety-check page. Anonymous browser creates require Turnstile and run a live safety check. Citation mode for arbitrary open-tier destinations still requires a signed-in account. Pointing a turl at another known URL shortener is unsupported and returns `400 {"error":"shortener_destination"}` — submit the original destination instead.

Creation outcomesCreate responses (including live SSE and MCP) report `resolves` as `redirect` or `preview`, explain the assigned tier with `trust_reason`, and include `expires_at`. Owner read/list responses report current `resolves` behavior, including `gone` later in the lifecycle. Use these fields instead of inferring behavior from `trust` or `scan_state`.

Citation capture is stagedCitation mode always remains preview-first. A configured account rollout may also admit an asynchronous source capture; the create response reports that admission separately. Capture history reports queue age, attempts, completion, partial, blocked, and failed states. Exact source HTML is download-only, public documents are forced attachments, and public replay is a newly serialized scriptless derivative on a separately configured content origin. No retention or durability SLA is implied.

Why links are immutableA short code always points at the destination it was minted for — turl never lets a code be re-pointed under the people who already shared or clicked it, so a trusted link can't be swapped for a malicious one later. There is no custom-alias or edit API today; owner-editable links may arrive as a future, explicitly granted capability.

```
curl -sS https://turl.ca/api/v1/expand/abc1234
```

```
curl -sS https://turl.ca/api/v1/account/links \
  -H 'Authorization: Bearer pbca_live_…'
```

## MCP for AI agents

turl.ca runs a Model Context Protocol server at `https://turl.ca/mcp`, so Claude Desktop, Cursor, the MCP Inspector, and other agents can shorten and manage links directly.

### Option A — OAuth (interactive clients)

Point your client at the MCP URL and it discovers the rest: it reads `https://turl.ca/.well-known/oauth-protected-resource/mcp`, registers with pastebin.ca (the authorization server), and runs OAuth 2.1 + PKCE. Add to your Claude Desktop config:

```
{
  "mcpServers": {
    "turl": {
      "url": "https://turl.ca/mcp"
    }
  }
}
```

Config path: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `~/.config/Claude/claude_desktop_config.json` (Linux). Cursor uses the same `mcpServers` shape in `~/.cursor/mcp.json`. Try it live with `npx @modelcontextprotocol/inspector`.

### Option B — API-key bearer (automation)

For headless use, send a minted key directly:

```
{
  "mcpServers": {
    "turl": {
      "url": "https://turl.ca/mcp",
      "headers": { "Authorization": "Bearer pbca_live_…" }
    }
  }
}
```

```
pbca account api-key create \
  --scopes link:read,link:create,link:delete \
  --audience https://turl.ca
```

### Tools

| Tool | Scope | Purpose |
| --- | --- | --- |
| `whoami` | — | Echo account id, scopes, audience, and token kind. |
| `create_link` | `link:create` | Shorten a URL; returns resolution, trust reason, safety state, and expiry. |
| `get_link` | `link:read` | Fetch one link with its current resolution behavior. |
| `list_my_links` | `link:read` | List links with current resolution behavior; cursor-paginated. |
| `delete_link` | `link:delete` | Retire one of the caller's links. |

## OAuth & DPoP

pastebin.ca is the OAuth authorization server; turl.ca is a protected resource and never mints tokens.

- OAuth tokens are **audience-bound** to `https://turl.ca/mcp` (RFC 8707) and honored only on the MCP route.
- If a key is **DPoP-bound** (RFC 9449), every MCP request must carry a matching ES256 proof; replays are rejected. Plain bearer keys work without DPoP.
- Revoke a key or connected app anytime from your pastebin.ca account.

## Discovery

| Document | URL |
| --- | --- |
| OpenAPI 3.1 | [`/api/v1/openapi.json`](/api/v1/openapi.json) |
| Agent card | [`/.well-known/agent.json`](/.well-known/agent.json) |
| MCP protected resource | [`/.well-known/oauth-protected-resource/mcp`](https://turl.ca/.well-known/oauth-protected-resource/mcp) |
| LLM site map | [`/llms.txt`](/llms.txt) |


---

---
title: "turl.ca error help"
source: "https://turl.ca/docs/errors"
---
[Home](/) › [Docs](/docs) › Error help

# Error help

Ran into an error on turl.ca? Look it up here.

Error codes

## Find a plain-language explanation

If turl.ca shows an error code, these pages explain what it means, why it happened, and what you can try next.

Most people only need the codes listed here. Uncommon technical codes have a shorter fallback page when they are real turl.ca errors.

7 errors

## Account and sign-in

[**Please sign in first** `auth_required` The action needs a signed-in account or API authorization before it can continue.](/docs/errors/auth_required)[**This request needs authorization** `unauthorized` The site could not find a valid account session or API token for this request.](/docs/errors/unauthorized)[**The API token is not valid** `invalid_token` An API token was sent, but it could not be accepted for this site.](/docs/errors/invalid_token)[**The API sign-in proof could not be verified** `invalid_dpop_proof` The site could not verify the extra sender proof attached to an API request.](/docs/errors/invalid_dpop_proof)[**This token cannot do that** `insufficient_scope` You are signed in, but the token or session does not include permission for this action.](/docs/errors/insufficient_scope)[**This action is not allowed** `forbidden` The site understood the request but refused to perform the action.](/docs/errors/forbidden)[**This account cannot use that feature** `account_banned` The signed-in account is restricted from creating or changing content right now.](/docs/errors/account_banned)

2 errors

## API keys and permissions

[**That API permission is not available** `invalid_scope` The requested API key permission is not one this site can grant.](/docs/errors/invalid_scope)[**There are too many API keys** `too_many_keys` This account has reached the API key limit for this site.](/docs/errors/too_many_keys)

6 errors

## Request format and limits

[**The request could not be read** `invalid_json` The site could not understand the JSON sent by the browser, app, or tool.](/docs/errors/invalid_json)[**The request is missing required details** `invalid_request` The site received a request, but some required fields were missing or invalid.](/docs/errors/invalid_request)[**The upload was too large** `request_too_large` The request was bigger than this site can accept in one attempt.](/docs/errors/request_too_large)[**That action is not available here** `method_not_allowed` The request used a method this route does not accept.](/docs/errors/method_not_allowed)[**The request came from the wrong place** `bad_origin` The site blocked the request because its origin did not match this site.](/docs/errors/bad_origin)[**Please slow down** `rate_limited` This site received too many requests in a short time.](/docs/errors/rate_limited)

1 error

## Finding content

[**That page or item could not be found** `not_found` The link points to something this site cannot find or no longer serves.](/docs/errors/not_found)

2 errors

## Service availability

[**The service is temporarily unavailable** `temporarily_unavailable` A required service could not answer in time, so the request could not be completed.](/docs/errors/temporarily_unavailable)[**A required service is unavailable** `unavailable` The site could not reach a service it needs to finish this action.](/docs/errors/unavailable)

11 errors

## Shortening a link

[**Paste a URL to shorten** `missing_url` turl.ca needs a destination URL before it can create a short link.](/docs/errors/missing_url)[**That link could not be read** `invalid_url:unparseable` The destination does not look like a complete web address that turl.ca can shorten.](/docs/errors/invalid_url%3Aunparseable)[**Use an https:// link** `invalid_url:scheme_not_https` turl.ca only shortens secure https:// destinations.](/docs/errors/invalid_url%3Ascheme_not_https)[**That URL is too long** `invalid_url:too_long` The destination address is longer than turl.ca can safely accept.](/docs/errors/invalid_url%3Atoo_long)[**Remove spaces from the URL** `invalid_url:control_or_space` The destination contains spaces or hidden characters that make it unsafe to shorten.](/docs/errors/invalid_url%3Acontrol_or_space)[**Remove sign-in details from the URL** `invalid_url:userinfo` The destination includes username or password-style text before the site name.](/docs/errors/invalid_url%3Auserinfo)[**Use the normal website address** `invalid_url:non_standard_port` The destination uses a custom port, which turl.ca does not shorten.](/docs/errors/invalid_url%3Anon_standard_port)[**Use a public site name** `invalid_url:ip_literal` turl.ca needs a normal public hostname, not a raw IP address.](/docs/errors/invalid_url%3Aip_literal)[**That destination is local or private** `invalid_url:local_host` The URL points at a local or private name that other people cannot safely open.](/docs/errors/invalid_url%3Alocal_host)[**Do not shorten a turl.ca link** `invalid_url:self_target` A turl.ca short link cannot point back at turl.ca or its preview host.](/docs/errors/invalid_url%3Aself_target)[**Paste the original destination URL** `shortener_destination` turl.ca does not shorten links that already point at another public URL shortener.](/docs/errors/shortener_destination)

1 error

## Account and access

[**Sign in to request citation evidence** `account_required` Citation mode for an unverified destination needs a free signed-in account.](/docs/errors/account_required)

4 errors

## Safety checks

[**This destination looks unsafe** `destination_flagged` A safety check flagged the destination, so turl.ca did not create the short link.](/docs/errors/destination_flagged)[**This destination is not allowed** `banned_host` The destination host is blocked from new turl.ca links.](/docs/errors/banned_host)[**This network cannot create links right now** `banned_ip` turl.ca is blocking link creation from this network for the moment.](/docs/errors/banned_ip)[**The redirect path could not be verified** `unsupported_redirect_chain` The destination redirects in a way turl.ca cannot clearly verify for visitors.](/docs/errors/unsupported_redirect_chain)

4 errors

## Live safety check

[**The quick security check did not pass** `turnstile_failed` The browser check could not confirm that the anonymous request should continue.](/docs/errors/turnstile_failed)[**The safety check took too long** `scan_timeout` turl.ca could not finish checking the destination before the live request timed out.](/docs/errors/scan_timeout)[**Too many checks are running** `too_many_live_checks` There are already several live safety checks running for this browser or account.](/docs/errors/too_many_live_checks)[**Something went wrong while shortening** `internal_error` turl.ca hit an unexpected problem before it could create the short link.](/docs/errors/internal_error)

