---
title: Claim, do your thing, finish
description: How start/finish works, why it's built that way, and how to write a consumer that behaves well.
---

{/* Keep in sync with api/src/lib/queue-controls.js (the semantics) and
    public/openapi.yaml (the operations) - maintained by hand. */}

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

1. **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.

2. **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.

3. **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

```text
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.
