Clip API
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.
Clients
Section titled “Clients”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 |
Tokens
Section titled “Tokens”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.
Connect flow
Section titled “Connect flow”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 ofchrome.identity.getRedirectURL(path), that ishttps://<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... WhenCLIP_EXTENSION_IDSis set (a comma-separated Pages variable), the extension ID must be one of them. - For
safari,iosandmac, exactlysponge://connect.
- For
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.
Cookie routes (the account page)
Section titled “Cookie routes (the account page)”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.
POST /api/clip/tokens
Section titled “POST /api/clip/tokens”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>" }.
GET /api/clip/tokens
Section titled “GET /api/clip/tokens”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.
DELETE /api/clip/tokens/:id
Section titled “DELETE /api/clip/tokens/:id”Reply: 200 { "ok": true }. An unknown ID, or another account’s, gets 404 not_found.
Bearer routes (clients)
Section titled “Bearer routes (clients)”Send Authorization: Bearer spk_…. These routes never accept cookies.
CORS:
- Extension origins get their
Originechoed inAccess-Control-Allow-OriginwithVary: Origin. The accepted forms arechrome-extension://<32 letters a–p>andsafari-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-Credentialsis never sent.OPTIONSanswers the preflight with 204. It allowsAuthorization, Content-Typeand the route’s method.
POST /api/clip/save
Section titled “POST /api/clip/save”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, withRetry-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" }.
GET /api/clip/me
Section titled “GET /api/clip/me”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.
Errors
Section titled “Errors”| 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 |
How a clip runs
Section titled “How a clip runs”- Pages hashes the bearer token and looks it up in D1, together with its enabled account. It then applies the rate limits.
- Pages forwards
{ url, selection }to the account’s Durable Object. The forward carries the token and its keyed hash, because the object holds noAUTH_SECRET. - The object checks that the token’s directory row belongs to the account it holds. It then runs
createSaveRequestin 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.