Skip to content

Clip API

Markdown · Agent index

Browser extensions and native share targets save links to a Sponge account with a clip token. A clip lands in the same save queue as the web library’s Save a link, and a connected Sponge device captures it. A clip token can only queue links and name the account. It cannot read the library, sync, publish or change the account.

Origin: https://account.sponge.computer. Every error body is { "error": code }, using the codes listed at the end.

client Default name Connects through
chrome Sponge for Chrome chrome.identity.launchWebAuthFlow
safari Sponge for Safari ASWebAuthenticationSession, callback scheme sponge
ios Sponge for iPhone ASWebAuthenticationSession, callback scheme sponge
mac Sponge for Mac ASWebAuthenticationSession, callback scheme sponge

A token is spk_ followed by 32 random bytes in base64url, 47 characters in all, matching /^spk_[A-Za-z0-9_-]{43}$/. Sponge keeps only a keyed hash of it, so the token is shown once, when it is minted. Tokens do not expire. A token stops working in any of these cases:

  • It is revoked under Connected apps on the account page.
  • The account’s Sponge key is replaced.
  • The account is disabled.
  • It is evicted. An account holds at most 20 tokens; minting a 21st removes the oldest in the same write.

A rejected token is always 401 { "error": "invalid_token" }. When a client gets that answer, it should forget the token and connect again.

GET /connect?client=chrome&state=<state>&redirect_uri=<redirect>

The query must hold exactly these three parameters:

  • state: 8–256 characters from [A-Za-z0-9._~-]. The client generates it and checks it when the redirect comes back.
  • redirect_uri:
    • For chrome, the value of chrome.identity.getRedirectURL(path), that is https://<32-letter extension id>.chromiumapp.org/<path>. The path can use only [A-Za-z0-9._~/-], up to 64 characters, with no query and no ... When CLIP_EXTENSION_IDS is set (a comma-separated Pages variable), the extension ID must be one of them.
    • For safari, ios and mac, exactly sponge://connect.

Any other query is refused with 400 and the page is not shown. The client never chooses an arbitrary redirect.

When the person is signed out, the page signs them in first: with a Sponge key, a passkey or an email code, or by creating an account. The connect question then appears on the same page. It reads “Connect Sponge for Chrome?” and offers Allow or Cancel.

  • Allow mints a token (POST /api/clip/tokens), then the page navigates to <redirect_uri>#token=<spk_…>&state=<state>.
  • Cancel navigates to <redirect_uri>#error=access_denied&state=<state>.

The result travels in the fragment, so it never reaches a server. The page sends no Referer and cannot be framed.

These routes need the account session cookie. A POST or DELETE also needs Origin: https://account.sponge.computer and X-CSRF-Token: <session csrf>, as other web writes do. They are limited to 20 changes a minute per account.

Body: { "client": "chrome" | "safari" | "ios" | "mac", "name"?: string }. The name is up to 64 characters and may not contain control characters. Without one, the client’s default name is used.

Reply: 201 { "token": "spk_…", "id": "<22 chars>" }.

Reply: 200 { "tokens": [{ "id", "client", "name", "createdAt", "lastUsedAt" }] }, newest first. Both dates are ISO 8601. lastUsedAt is null until the token’s first use, then is updated to the minute, at most once a minute.

Reply: 200 { "ok": true }. An unknown ID, or another account’s, gets 404 not_found.

Send Authorization: Bearer spk_…. These routes never accept cookies.

CORS:

  • Extension origins get their Origin echoed in Access-Control-Allow-Origin with Vary: Origin. The accepted forms are chrome-extension://<32 letters a–p> and safari-web-extension://<id>.
  • A caller that sends no Origin (a native app or a share extension) is served without CORS headers.
  • Any other origin, web pages included, gets 403 origin_not_allowed.
  • Access-Control-Allow-Credentials is never sent.
  • OPTIONS answers the preflight with 204. It allows Authorization, Content-Type and the route’s method.

Body (Content-Type: application/json, at most 4 MiB + 64 KiB):

{ "url": "https://…", "title"?: "…", "selection"?: "…", "html"?: "…" }
Field Rule What happens to it today
url Required. A credential-free http(s) URL; the fragment is dropped (normalizeSaveUrl). Queued.
title String, ≤ 1000 characters. Validated, then dropped. The device takes the page’s own title when it captures.
selection String, ≤ 20 000 characters. Kept as the save’s note. Control and format characters become spaces, it is trimmed, and it is cut to 500 characters, ending in … when cut.
html String, ≤ 4 MiB of UTF-8, with no NUL. Validated, then dropped. The device captures the page itself.

Any other field gets 400 invalid_request. The save queue carries only a URL and a note, so title and html are accepted now so that clients need not change when the queue can carry them.

Reply: 201 { "request": WebSaveRequest, "existing": false } when the link was queued, or 200 { "request": …, "existing": true } with the open request that already holds the same URL. request is the web’s own save request, described in Ways to save.

Limits:

  • 30 clips a minute per account (429 rate_limited, with Retry-After).
  • The queue’s own limit of 500 open links (409 queue_full).
  • With Sponge Plus billing, the free plan’s item limit: 402 { "error": "limit_reached", "upgradeUrl": "https://account.sponge.computer/plus" }.

Reply: 200 { "email": string | null }. The email is null for an account that signs in only with its key or a passkey. Once Sponge Plus is for sale (billing configured), the reply also carries plan: "plus" for a paid or comped account, "free" for one under the free plan’s item limit. Before that, every account is unlimited and plan is absent.

Status error When
400 invalid_url url is missing, not http(s), or carries credentials
400 invalid_request Unknown fields, wrong types or bad JSON
401 invalid_token Missing, malformed, revoked, evicted or replaced token
401 sign_in_required Cookie routes, when signed out
402 limit_reached The free plan is full; see upgradeUrl
403 origin_not_allowed An origin these routes do not serve
403 csrf_not_accepted Cookie routes, with a missing or stale CSRF token
404 not_found
405 method_not_allowed
409 queue_full 500 links already wait for a device
413 request_too_large The body or html is over its limit
415 json_required
429 rate_limited With Retry-After
503 unavailable With Retry-After; try again
  1. Pages hashes the bearer token and looks it up in D1, together with its enabled account. It then applies the rate limits.
  2. Pages forwards { url, selection } to the account’s Durable Object. The forward carries the token and its keyed hash, because the object holds no AUTH_SECRET.
  3. The object checks that the token’s directory row belongs to the account it holds. It then runs createSaveRequest in a batch guarded on the account being enabled.

An account Pages still serves itself (storage='d1') runs createSaveRequest in Pages instead, in a batch guarded on the token row.

Documentation drafted with AI and checked against the product source.