Skip to content
TipPage Docs
Esc
navigateopen⌘Jpreview
On this page

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: false and 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 (or current_order_id on 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 back 409 already_playing and current_order_id is 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 409 as normal operation. queue_paused and already_playing mean 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.

Was this page helpful?