onelink.ninja logo onelink.ninja

API overview

A plain JSON REST API that mirrors the web flow: create a list, get an edit credential back, optionally claim it later. No API key, no signup, no OAuth dance — you can create a list with one curl.

Agent-ready out of the box

There’s no SDK, plugin or key to install. An agent told to “use onelink.ninja” can do the whole thing on its own:

  1. Discover. /llms.txt is a plain-text guide to every endpoint, with the error codes and the rules about credentials. /openapi.json is the same API as an OpenAPI 3.1 document, with the real limits in the schemas. Every page links to both, in a visible line of text, in its <head> and in a Link header.
  2. Create. One unauthenticated POST /api/lists call. No key to provision and no account to set up first.
  3. Hand back. The response contains publicUrl for sharing and editUrl for the human. The agent should return both.
  4. Keep editing. With the editToken, the same agent can PATCH the list or append links in later turns.
  5. Read. Any list is available as plain Markdown at /l/{id}.md (more on that below), so reading a list costs a fraction of the tokens HTML would.

Errors use stable machine-readable codes, and a 429 tells the agent how long to wait, so a well-behaved agent can recover on its own. The user guide for this flow has example prompts.

Your first request

curl -X POST https://onelink.ninja/api/lists 
  -H 'content-type: application/json' 
  -d '{"title":"My list","links":[{"url":"https://example.com","label":"Example"}]}'
{
	"id": "Q20d9AsqNh",
	"publicUrl": "https://onelink.ninja/l/Q20d9AsqNh",
	"editUrl": "https://onelink.ninja/l/Q20d9AsqNh/edit?token=xK_2_OQe-STVdmd_jiZdUhBoJefLYAmarx",
	"editToken": "xK_2_OQe-STVdmd_jiZdUhBoJefLYAmarx",
	"markdownUrl": "https://onelink.ninja/l/Q20d9AsqNh.md"
}

Full endpoint list in the API reference, or machine-readable in /openapi.json.

Three ways in

Not every agent can call an API. Use the first that works where you run:

You can…Use
make HTTP requeststhe JSON API (this page)
connect to remote MCP serversthe MCP server at /mcp
only write text, like a chatbota prefill link for the person

If your environment can’t reach onelink.ninja, don’t keep retrying. Hand over a prefill link instead.

MCP server

https://onelink.ninja/mcp is a remote MCP server (Streamable HTTP, stateless, no auth). Its tools wrap the API with the same validation and limits:

ToolDoes
create_listcreates a list and returns the share link, edit link and token
get_listreads a list
add_linkappends one link
update_listchanges title, description or links; links replaces all of them
delete_listdeletes a list
prefill_linkbuilds a prefill link without saving anything

Edit tools take the editToken as edit_token. Claimed lists can’t be edited over MCP. To add it to Claude or another assistant, see Connect your AI assistant.

Prefill links

A prefill link opens the new-list form already filled in. Nothing is saved until the person reviews it and clicks Publish list. It is the way for assistants that can’t send requests to still produce a list:

https://onelink.ninja/new#data=<URL-encoded JSON>

The JSON is the same body as POST /api/lists, plus a version: {"v":1,"title":"…","links":[{"url":"…","label":"…"}]}. URL-encode it and put it after #data=, as the last thing in the link. The data sits in the fragment (after #), so it never reaches our server or its logs. Links with an invalid URL are dropped and over-long text is cut when the form opens, and the form says so. Keep the link under about 8 KB.

It’s plain JSON on purpose: a chat assistant can write it without running code, and anyone can see which URLs a link contains before opening it. The form also reads JSON that wasn’t fully encoded.

const list = { v: 1, title: 'Grüße', links: [{ url: 'https://example.com', label: 'Example' }] };
const link = 'https://onelink.ninja/new#data=' + encodeURIComponent(JSON.stringify(list));
import json, urllib.parse
lst = {"v": 1, "title": "Grüße", "links": [{"url": "https://example.com", "label": "Example"}]}
link = "https://onelink.ninja/new#data=" + urllib.parse.quote(json.dumps(lst), safe="")

Authentication

Reads are open. Writes need one of two things:

  • Authorization: Bearer {editToken} — the token returned when the list was created. Works until the list is claimed.
  • A session cookie owning the list — for a claimed list, from a signed-in browser.

Once a list is claimed its editToken is destroyed and stops working. That is the same rule the web edit page follows.

Hand the editToken back to whoever you made the list for, and tell them it is the only way back in.

Edit tokens are random and independent of the list id. Knowing a public URL tells you nothing about its token.

Reading lists as Markdown

Often you don’t need the JSON API at all. Every list is served as plain Markdown, which is the cheapest way for an LLM or a script to read it:

URLServes
/l/{id}.mdtext/markdown, the raw list
/@{username}/{slug}.mdthe same, for a claimed list with a custom URL
/l/{id}/markdownan HTML page showing that text in a copy box, for people
# Tools I actually use

The short list, not the bookmark graveyard.

- [Svelte](https://svelte.dev) — The framework this runs on
- [Bun](https://bun.sh) — Runtime and package manager

Every public list page advertises its Markdown version in the head, so agents can find it without guessing:

<link rel="alternate" type="text/markdown" href="/l/{id}.md" />

The HTML /markdown view exists because browsers tend to download text/markdown instead of displaying it.

Requests

Writes must send content-type: application/json. A form-encoded body is rejected rather than parsed, so a stray HTML form can never reach a write endpoint.

Errors

Every failure has the same shape. Branch on code — the message is prose and may be reworded.

{
	"error": {
		"code": "invalid_credentials",
		"message": "That editToken is not valid for this list."
	}
}
codestatusmeans
invalid_body400not JSON, wrong content-type, or failed validation
missing_credentials401a write with no token and no session
invalid_credentials403wrong token, or a session that does not own the list
not_found404no list with that id
method_not_allowed405a method the URL lacks, e.g. reading /api/lists
rate_limited429too many creates from your address

On a validation failure, error.details carries the specific field problems, each with a path such as ["links", 0, "url"].

Limits

Titles and labels are at most 200 characters, descriptions at most 1000. A list holds at most 20 links. Link URLs must use http, https, mailto or tel.

Note that a wrong credential is a 403, not a 404. Whether a list exists is already public via GET, so pretending otherwise would only make your own bugs harder to find.

Rate limits

Creating a list is limited to 10 per minute per IP — generous for a person, dull for a script. A 429 means wait, not that the request was malformed; the Retry-After header and the message tell you for how long. Creates over MCP have their own, higher limit, because hosted assistants share a few IP addresses between all their users. Successful creates carry X-RateLimit-Remaining.

Nothing else is limited: the other endpoints need a credential that had to be handed out, or are reads.

CORS

Every endpoint sends Access-Control-Allow-Origin: * and answers preflight, so browser extensions and bookmarklets work from any origin.

There is deliberately no Access-Control-Allow-Credentials. Browsers refuse to send cookies to a wildcard origin, which means another site cannot use a visitor’s signed-in session to edit their lists. Cross-origin callers authenticate with a Bearer token instead — one they can only have if somebody gave it to them.