API

Save, search, and manage your bookmarks programmatically or via AI agent. Use API keys to authenticate and build your own integrations.

Authentication

All requests require a Bearer token. Generate an API key from Settings → API Keys in your dashboard.

Authorization: Bearer runa_sk_...

Base URL

https://api.onruna.com

Endpoints

POST/v1/enrich30 req/min

Enrich a URL with metadata without saving it. Returns OG data, link type, and type-specific enrichment.

Request body

{
  "url": "https://example.com/article"
}

Response

{
  "title": "Example Article",
  "description": "...",
  "imageUrl": "https://...",
  "favicon": "https://...",
  "domain": "example.com",
  "linkType": "article",
  "content": "...",
  "typeMeta": null,
  "attachments": null,
  "isNsfw": false
}
POST/v1/links30 req/min

Create a new bookmark. Automatically enriches the URL with metadata, AI summary, and tags.

Request body

{
  "url": "https://example.com/article",
  "source": "manual"  // optional: manual | x-bookmark | email | feed
}

Response

{
  "id": "k57abc...",
  "url": "https://example.com/article",
  "title": "Example Article",
  "domain": "example.com",
  "linkType": "article",
  "status": "inbox",
  "enrichmentStatus": "enriching"
}
GET/v1/links60 req/min

List bookmarks filtered by status. Defaults to inbox.

Parameters

Query params:
  ?status=inbox    // inbox | archived | trash

Response

{
  "links": [
    { "id": "...", "url": "...", "title": "...", "status": "inbox", ... }
  ]
}
GET/v1/links/:id60 req/min

Get a single bookmark by ID.

Response

{
  "id": "...",
  "url": "...",
  "title": "...",
  "description": "...",
  "domain": "...",
  "status": "inbox",
  ...
}
POST/v1/links/search30 req/min

Full-text + semantic hybrid search powered by Meilisearch. Supports filtering, sorting, and pagination.

Request body

{
  "q": "react server components",  // optional, empty for browse
  "filter": "status = \"inbox\"",   // optional, Meilisearch filter
  "sort": ["createdAt:desc"],       // optional
  "limit": 20,                      // optional, 1-100 (default 20)
  "offset": 0                       // optional, for pagination
}

Response

{
  "hits": [
    {
      "id": "j97...",
      "url": "https://...",
      "title": "...",
      "description": "...",
      "domain": "example.com",
      "source": "manual",
      "itemType": "article",
      "status": "inbox",
      "isNsfw": false,
      "createdAt": 1774697115919,
      "summary": "...",
      "tags": ["typescript", "react"]
    }
  ],
  "estimatedTotalHits": 42,
  "processingTimeMs": 3,
  "offset": 0,
  "limit": 20
}

Search reference

Filterable attributes

status      "inbox" | "archived" | "trash"
itemType    "article" | "tweet" | "reddit" | "youtube" | "github" | "pdf" | "image"
isNsfw      true | false
source      "manual" | "x-bookmark" | "email" | "feed" | "browser-import"
createdAt   unix timestamp in milliseconds
tags        array of tag name strings

Filter operators

=, !=, >, >=, <, <=    Comparison
TO                     Range (inclusive): createdAt 1711900800000 TO 1712505600000
IN                     Set membership: status IN ["inbox", "archived"]
EXISTS, NOT EXISTS     Field presence: tags EXISTS
AND, OR, NOT          Logical combinators
( )                    Grouping

Filter examples

status = "inbox"
itemType = "tweet" AND status != "trash"
status IN ["inbox", "archived"]
createdAt > 1711900800000
tags = "typescript"
source = "x-bookmark" OR source = "browser-import"
(itemType = "article" OR itemType = "youtube") AND status = "inbox"
NOT tags EXISTS

Sortable attributes

["createdAt:desc"]   // newest first
["createdAt:asc"]    // oldest first
POST/v1/files10 req/min

Upload a PDF or image file. Accepts multipart form data. Supported types: PDF, JPEG, PNG, WebP, GIF, HEIC. Max 50MB.

Request body

Content-Type: multipart/form-data

  file: <binary>

Response

{
  "id": "k57abc...",
  "filename": "document.pdf",
  "mimeType": "application/pdf",
  "size": 1048576,
  "itemType": "pdf",
  "status": "inbox",
  "enrichmentStatus": "enriching"
}
PATCH/v1/links/:id30 req/min

Update bookmark metadata or status. Supports partial updates.

Request body

{
  "title": "New title",        // optional
  "description": "...",        // optional
  "status": "archived",        // optional: inbox | archived | trash
  "isNsfw": false              // optional
}

Response

{ "success": true }
DELETE/v1/links/:id20 req/min

Permanently delete a bookmark.

Response

{ "success": true }

Rate limits

All endpoints are rate limited per API key using a sliding window. When you exceed the limit, the API returns a 429 Too Many Requests response with these headers:

X-RateLimit-Limit: 60       // max requests in window
X-RateLimit-Remaining: 42   // requests left
X-RateLimit-Reset: 1711...  // window reset (unix ms)
Retry-After: 23              // seconds until retry

Quick example

# Save a link
curl -X POST https://api.onruna.com/v1/links \
  -H "Authorization: Bearer runa_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/article"}'

# Search links
curl -X POST https://api.onruna.com/v1/links/search \
  -H "Authorization: Bearer runa_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"q": "react", "filter": "status = \"inbox\"", "limit": 10}'

# Browse recent inbox (no query, just filter + sort)
curl -X POST https://api.onruna.com/v1/links/search \
  -H "Authorization: Bearer runa_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"filter": "status = \"inbox\"", "sort": ["createdAt:desc"], "limit": 20}'

# Upload a file
curl -X POST https://api.onruna.com/v1/files \
  -H "Authorization: Bearer runa_sk_..." \
  -F "file=@document.pdf"

Interactive docs

Full Swagger/OpenAPI docs are available at https://api.onruna.com/swagger