Changelog
Routes, request fields, error codes and prices — the things you would have to change something for. Refactors, performance work and everything else that leaves the surface where it was are not here, on the grounds that a changelog you have to skim is one you stop reading.
Everything below predates the first key we issued, so none of it has ever been a migration for anybody. It is here because the shape of an API is easier to trust when you can see how it got that shape. From here on, a breaking change gets a new prefix rather than a new meaning under the old one.
Whole video objects for ids you name, a share-link shortener, and the recommendation feed — both cold and seeded from a video. /videos and /links/shorten are the first two batch-class routes; the rest are lookups.
/tiktok/users became /v1/tiktok/users, and so on for every path. The version prefix is where a breaking change will be visible if one ever has to happen; nothing will change shape underneath it.
Every route but /healthz now takes an Authorization: Bearer sor_live_… key. 401 unauthenticated covers every key problem there is; 403 key_revoked is the one worth telling apart.
Each route carries a cost class — lookup, batch or search — and each plan a daily ceiling and a burst limit. New codes: 402 insufficient_funds, 403 not_on_plan, 429 rate_limited, 429 daily_limit_reached, 503 metering_unavailable. A call is charged only when it answers 200; a failure is recorded at $0.00 and still counts against the day.
A response bigger than we will pass through now has a code of its own rather than arriving as a generic upstream_error. Not charged.
/search/videos, /search/users, /search/music, /search/places, /search/photos and /search/live. Page two and later must carry the first page’s x-tt-logid back as search_id, or the call is refused with 400 search_id_required before it costs anything. Videos, places, photos and live page on offset; music and users page on cursor, and the difference is carried out to the caller rather than smoothed over.
A 200 now carries the upstream body and content type unchanged, marked with x-sorina-shape: upstream, instead of a projection of ours. Anything reading a reshaped field had to move to the platform’s own name for it.
There is no longer any way to hand us an arbitrary upstream path. Every endpoint is a named route with a validated body — which is what makes it possible to price one, test one, and say what it returns.
The all-results tab. It accepts offset: 0 and answers 400 offset_unsupported for anything else, because a non-zero offset starts a fresh search rather than continuing one. Its body arrives framed and is handed back as a JSON array of frames.
/comments, /comments/replies, /music, /music/videos and /playlists. /comments/replies takes both comment_id and aweme_id: a comment does not identify its own video in that request.
Named POST routes with JSON bodies replaced query-string GETs. A GET against a named route is a 405. Arguments in a body rather than a URL keeps them out of every access log on the path.
What counts as breaking
Removing a route, removing a request field, or changing what an existing code means. Those get a new version prefix. Adding a route, adding an optional field, or adding a new code does not — write your error handling so an unfamiliar code falls through to a sensible default rather than crashing.
The bodies we pass through are not ours, and we do not version them. A platform can change the shape of its own answer without telling either of us. Where that has already happened, or where we have not seen enough to be sure, the endpoint pages say so rather than guessing.