Claim, do your thing, finish
How start/finish works, why it's built that way, and how to write a consumer that behaves well.
The API lets your own code play tips instead of our overlay. The flow is three calls: claim a TTS as now playing, run whatever effect you’ve built, then finish it. TipPage handles the payment, the screening and filtering, and the queueing. Your code only has to handle what happens when a tip plays - audio through a phone, a light rig, a display, an overlay you built yourself. The overlay we ship uses this same protocol, so you can replace it completely if you want to.
The three steps
Claim it
POST /v1/tts/{orderId}/start marks the TTS as now playing. The
dashboard shows it as current, the queue.tts.started webhook fires,
and while you hold the claim any other consumer that tries to claim
gets 409 already_playing. You can claim any queued item, in any
order you like.
Do your thing
Play the pre-rendered audio from tts_url, or synthesize your own
from name, amount, and message. Ring hardware, update a display,
whatever you’ve built. There’s no timeout; the claim holds until you
finish or release it.
Finish it
POST /v1/tts/{orderId}/finish moves the item from the queue into
history, frees the now-playing slot so the next item can start, and
fires queue.tts.finished. Until you finish (or the streamer skips),
nothing else plays.
The media queue uses the same protocol - POST /v1/media/{orderId}/start
and /finish - with one difference, covered below.
When it doesn’t go well: release
Finishing means “this played” - the tip lands in history and won’t play again. Sometimes that’s not what happened: your audio device errored, the video refused to load, your process is shutting down halfway through an item. For those cases there’s a third call:
POST /v1/tts/{orderId}/release (and /v1/media/{orderId}/release) is
the undo of start. It drops your now-playing claim without finishing:
the item stays in the queue at its position, the slot frees, and the item
can be claimed again - by you once you’ve recovered, by another consumer,
or by the overlay. queue.tts.released / queue.media.released fires so
other listeners know the slot opened up.
Release is deliberately narrow and safe:
- It only drops a claim that points at the order id you name. It can never kick out a claim held by a different item, so a stale release from a crashed run can’t disturb whatever is playing now.
- Releasing something you don’t hold is a no-op, not an error - the
response says
released: falseand nothing changes. That makes it safe to retry and safe to call “just in case” during cleanup. - The tip is never lost or double-counted: it either plays later (someone
claims it again) or the streamer removes it. History only ever gets it
once, via
finish.
The rule of thumb: finish when the tip played, release when it didn’t. If your effect died halfway through and replaying the whole tip would be worse than losing the tail end, finishing is still fine - that’s a judgement call your consumer gets to make.
How the calls behave
start can be refused. You’ll get 409 queue_paused while the streamer
has the queue paused, 409 already_playing while another item holds the
slot, and 404 not_in_queue when no queued item has that order id -
whether it was finished, removed, or never existed.
finish never refuses. It works while the queue is paused, it works on
items that were never started, and calling it on an item that’s already
gone simply returns tip: null instead of erroring, so it’s safe to
retry. The reason for the difference: when the streamer pauses mid-item,
your consumer still needs to file that item away once its audio ends.
If pausing blocked the finish, the item would sit in the queue and play a
second time after unpausing. So pausing stops new claims and nothing else.
release follows the same never-refuses philosophy as finish: it
answers 200 whether or not there was a claim to drop, and tells you
which happened via released.
Every state change - your claims and finishes, the overlay’s, the dashboard’s skip button - goes through one per-streamer lock, and there is a single now-playing slot. Two consumers can’t play the same item at once, and a finish can’t land halfway through someone else’s start. You get that guarantee without writing any coordination code of your own.
Your consumer also isn’t registered with the platform in any way, and the platform keeps no state about it. If your process crashes after claiming, the item stays in the queue, still marked as playing. The streamer’s skip button clears it, or your process can pick it back up when it restarts (see below). Losing your consumer never loses a tip.
The media claim skips the pause and already-playing checks. That matches how the overlay’s media player has always worked - pacing between videos is left to whoever is playing them.
Writing a consumer that behaves
- Finish or release everything you claim. The slot stays taken until you let it go or the streamer skips. Played? Finish. Failed before it really played? Release, so the tip isn’t lost. Either way the queue keeps moving.
- Release on the way down. If your process is told to shut down while holding a claim, release it before exiting - the item goes back to the queue and whoever runs next plays it cleanly.
- Recover on startup. Read
GET /v1/tts/now(orcurrent_order_idon the queue). If something is marked playing that you don’t remember claiming, your previous run crashed mid-item: release it (or redo the effect and then finish). Similarly, if a claim comes back409 already_playingandcurrent_order_idis the item you were about to claim, the claim already went through on a previous attempt - carry on and play it. - Turn off the overlay’s TTS while your consumer runs. Otherwise both will try to claim the same items. The lock keeps that safe, but alerts will end up alternating between the two devices.
- Learn about new work from webhooks (
tip.created), or poll the queue every few seconds. Both are fine; polling needs no public endpoint. - Claim when you’re ready to play, not when the webhook arrives. Items wait in the queue as long as they need to.
- Treat
409as normal operation.queue_pausedandalready_playingmean back off and check again shortly, not retry in a tight loop.
The shape of a consumer
loop:
q = GET /v1/tts/queue
if q.is_paused or q.currently_playing or no items: wait, continue
tip = pick one (usually q.items[0])
POST /v1/tts/{tip.order_id}/start -> 409/404? continue
... your effect here ...
effect worked?
POST /v1/tts/{tip.order_id}/finish
else
POST /v1/tts/{tip.order_id}/release
Point that loop at a phone and you have a tip hotline; at a light rig, a lightshow; at a browser page you wrote, a full replacement for our overlay. Everything except the middle line can be copied as-is.