Single Serving

Reference

Docs.

One REST surface for items, one long-lived connection for everything that changes.

Authentication

The app issues a device token on first launch. Send it as a bearer token. There are no cookies and no session state on the server, so the token is the whole identity of a client. Without a valid one every path under /api/ answers 401.

Example request
curl https://tylerdyrden.online/api/v3/items \
  -H "Authorization: Bearer $SS_DEVICE_TOKEN" \
  -H "Accept: application/json"

Items

MethodPathWhat it does
GET/api/v3/itemsUnread items, newest first. ?since= takes a cursor.
POST/api/v3/itemsSave a URL. Returns the item once the title has resolved.
POST/api/v3/items/:id/spendMark as read. The item leaves at the next local midnight.
DELETE/api/v3/items/:idDrop it now, without reading.

Item shape

{
  "id": "it_8Fq2vK",
  "url": "https://slowmedia.blog/to-read-pile",
  "title": "Why your to-read pile is a to-do list in disguise",
  "saved_at": "2026-10-02T09:14:22Z",
  "expires_at": "2026-11-01T09:14:22Z",
  "state": "unread"
}

Errors

Errors are JSON with the same shape everywhere, so a client never has to guess what it is parsing:

{ "error": "unauthorized", "message": "Missing or invalid device token." }

Sync

Polling works, but clients are expected to hold one connection instead and take changes as they arrive:

POST /cdn/v3/sync/
Content-Type: application/octet-stream

Frames are length-prefixed and carry the same objects as the REST surface. A client that drops reconnects with its last cursor and is sent what it missed — nothing lost, nothing replayed twice. A plain GET on that path is rejected; it only speaks the frame protocol.

Why it lives under /cdn

Historical, mostly. Sync and the static assets were put behind one cache so a single hostname covers both, and moving it now would break every shipped client for no real gain. Treat the prefix as an address, not a promise about caching: sync responses are never cached.

Limits

LimitValue
Saves per hour120 per device
Item retention30 days unread; until midnight once read
Payload64 KB per request
Open sync connections4 per account

Going over a limit returns 429 with Retry-After. Nothing is banned automatically; if something looks stuck, write to me.