API
Save, list and organize your Purl links and folders from your own code with the REST API and an API key.
Purl's REST API lets your own scripts and tools save links, list them, and keep them organized in folders, with the same rules as the app. Every request is scoped to the account that owns the API key.
The base URL is https://purl.live/api/v1. Every request needs an API key in the Authorization header.
Create a key in Purl under Settings โ Integrations. Keys start with purl_ and are shown once, so copy it somewhere safe. You can revoke a key from the same place at any time.
Send it as a bearer token:
curl https://purl.live/api/v1/links \
-H "Authorization: Bearer purl_your_key"Requests and responses are JSON. Send Content-Type: application/json with a body.
A link looks like this:
{
"id": "cm2x8k1q00001",
"url": "https://example.com/article",
"title": "An article worth keeping",
"description": "The page's description, when it has one.",
"favicon": "https://example.com/favicon.ico",
"thumbnail": "https://example.com/cover.jpg",
"domain": "example.com",
"contentType": "WEB",
"createdAt": "2026-10-10T09:41:00.000Z",
"folderId": null
}contentType is one of WEB, YOUTUBE, PDF or AUDIO. folderId is null when the link isn't in a folder.
GET /links returns your links, newest first.
| Query parameter | Description |
|---|---|
limit | How many to return, 1โ100. Defaults to 50. |
cursor | The nextCursor from the previous page. |
contentType | Only links of this type: WEB, YOUTUBE, PDF or AUDIO. |
folderId | Only links in this folder. |
curl "https://purl.live/api/v1/links?limit=20" \
-H "Authorization: Bearer purl_your_key"{
"data": [{ "id": "cm2x8k1q00001", "url": "https://example.com/article", "...": "..." }],
"nextCursor": "cm2x8k1q00000"
}Keep passing nextCursor as cursor until it comes back null.
POST /links saves a URL. Purl reads its title, description, favicon and thumbnail before it answers, so the response is the finished link. Pass folderId to save it straight into a folder.
curl https://purl.live/api/v1/links \
-H "Authorization: Bearer purl_your_key" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/article", "folderId": "cm2x8f0a00002" }'It answers 201 with the link. Saving a URL you already have doesn't create a copy: the existing link is refreshed, and when you pass a folderId it's moved there, with "moved": true in the response.
GET /links/{id} returns one link, or 404 if it isn't yours.
PATCH /links/{id} changes any of url, title, description and folderId. Send only the fields you want to change. "folderId": null takes the link out of its folder.
curl -X PATCH https://purl.live/api/v1/links/cm2x8k1q00001 \
-H "Authorization: Bearer purl_your_key" \
-H "Content-Type: application/json" \
-d '{ "title": "A better title", "folderId": "cm2x8f0a00002" }'DELETE /links/{id} deletes it for good and answers 204.
PATCH /links/bulk moves several links into a folder at once ("folderId": null takes them out of their folders):
curl -X PATCH https://purl.live/api/v1/links/bulk \
-H "Authorization: Bearer purl_your_key" \
-H "Content-Type: application/json" \
-d '{ "ids": ["cm2x8k1q00001", "cm2x8k1q00003"], "folderId": "cm2x8f0a00002" }'{
"moved": [{ "id": "cm2x8k1q00001", "previousFolderId": null }],
"notFound": ["cm2x8k1q00003"]
}DELETE /links/bulk with { "ids": [...] } deletes them and answers { "deleted": 2 }.
Ids you don't own are skipped, not errors: they come back in notFound (or simply aren't counted). Repeated ids count once.
A folder looks like this:
{
"id": "cm2x8f0a00002",
"name": "Reading list",
"slug": "reading-list",
"emoji": "๐",
"description": "Long reads for the weekend",
"isPublic": false,
"position": 1,
"linkCount": 12
}emoji is always set (๐ฆช when you haven't picked one). description is null when empty. position is your order: the one you see in Purl's folder menu.
GET /folders returns all your folders, in your order.
POST /folders with name (up to 60 characters, unique for you, ignoring case), and optionally emoji (a single emoji) and description (up to 160 characters). It answers 201 with the folder, which goes last in your order.
curl https://purl.live/api/v1/folders \
-H "Authorization: Bearer purl_your_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Reading list", "emoji": "๐" }'PATCH /folders/{id} changes any of name, emoji, description and isPublic. null clears emoji or description. "isPublic": true shares the folder: anyone with the link can read it at purl.live/@your-username/folder-slug (search engines don't index it). false makes it private again.
DELETE /folders/{id} deletes the folder and keeps its links, now outside any folder. Add ?withLinks=true to delete its links too. It answers { "deletedLinks": 0 } with the number of links deleted.
PUT /folders/order sets your order. Send every folder id, first to last:
curl -X PUT https://purl.live/api/v1/folders/order \
-H "Authorization: Bearer purl_your_key" \
-H "Content-Type: application/json" \
-d '{ "ids": ["cm2x8f0a00002", "cm2x8f0a00005", "cm2x8f0a00004"] }'It answers { "folders": [...] } in the new order. A list that's missing a folder, has an extra or repeated one, or one that isn't yours changes nothing and answers 400 with INVALID_ORDER. Fetch the folders again and retry.
Errors are JSON with an error message, and a code when there's something specific to act on:
{ "error": "You already have a folder with that name. Choose another.", "code": "NAME_TAKEN" }| Status | Meaning |
|---|---|
400 | The request isn't valid: a bad URL or JSON body, or a field out of bounds. Codes include NAME_EMPTY, NAME_TOO_LONG, INVALID_EMOJI, INVALID_DESCRIPTION, INVALID_ORDER, INVALID_IDS, TOO_MANY_IDS and INVALID_FOLDER. |
401 | The API key is missing, wrong or revoked. |
403 | A limit is reached (LIMIT_REACHED): 1,000 saved links or 100 folders per account. |
404 | The link or folder doesn't exist or isn't yours. |
409 | You already have a folder with that name (NAME_TAKEN). |
429 | Too many requests. Wait the number of seconds in the Retry-After header. |
- Each account can save up to 1,000 links and create up to 100 folders. Everyone gets the same limits, and Purl is free.
- Up to 120 requests a minute per account, of which 60 can be
POSTs. - The API allows cross-origin requests, so you can call it from a browser extension or a web page. Keep your key out of anything you publish.
Prefer to work from a chat? The MCP server gives Claude and other AI assistants the same tools.