---
title: Guides
description: Putting the API to work - the chat bot loop, viewer sub-reward credits, AI voices, and a worked end-to-end example.
---

## The chat bot: speak and manage commands

The `chat.command` [webhook](/api/webhooks#events) is one half of a loop - the
other half is making the bot talk back. Together they turn the bot into a
thin client for your own service:

- [`GET /v1/bot`](/api/reference/chat-bot/getbotstatus) - whether the bot is on,
  which channel it's in, and the identity it speaks as (the shared
  TipPageBot or the streamer's linked custom bot). Any valid key.
- [`POST /v1/chat/messages`](/api/reference/chat-bot/sendchatmessage) - the bot says
  your message in chat, verbatim (`chat:write`) - in every connected chat,
  or just Twitch or Kick with `platform`. Messages can't start with
  `/` or `!`, and sends have their own rate budget (20 per 30 seconds per
  streamer).
- [`/v1/commands`](/api/reference/chat-bot/listcustomcommands) - full CRUD over the
  bot's custom `!commands` (`commands:manage`), addressed by command name.
  The same rows as the dashboard's Chat bot page: responses, permission
  levels, cooldowns, aliases, `platforms` (which chats it answers in) and
  per-command enable switches, with the
  same [template variables](/chat-bot/variables) (`{user}`, `{args}`,
  `{pick:...}`, `{if:...}`, `{ai:...}`, ...). Changes reach chat within a
  few seconds. `chat.command` tells you which platform the viewer typed
  in, so a reply can go back to that chat alone.

So a game-integration can create `!loadout` at stream start, update its
response as the loadout changes, and delete it at stream end - or skip
stored responses entirely: catch `chat.command` on your server, compute
the answer, and POST it back through `/v1/chat/messages`. Built-in
commands, built-in aliases, and response lists stay dashboard surfaces
(command responses can still use `{list:name}` lists managed there).

## Viewers and sub-reward credits

Streamers with sub rewards enabled give subscribers free TTS/media credits;
viewers sign in to the tip page (with Twitch or Kick) to see and spend them. The
`viewers:read` and `viewers:manage` scopes put that whole system under
automation - award bonus credits from your own loyalty bot, sync balances
into Discord, or audit who's actually using their rewards.

- [`GET /v1/viewers`](/api/reference/viewers/listviewers) - everyone who has signed
  in, most recent first, with first-sign-in dates and credit balances. Every
  viewer object says which account they signed in with (`platform` +
  `platform_user_id`) and lists their `linked_accounts` on the other platform.
- [`GET /v1/viewers/{user}`](/api/reference/viewers/getviewer) - one viewer in full:
  profile, sign-in date, balance (including gift-sub credits still pending
  sign-in), the grant-by-grant ledger, and usage totals.
- [`GET /v1/viewers/{user}/rewards`](/api/reference/viewers/listviewerrewards) -
  their played sub-reward messages, paginated like `/tts/history`.
- [`POST /v1/viewers/{user}/credits/grant`](/api/reference/viewers/grantviewercredits) /
  [`revoke`](/api/reference/viewers/revokeviewercredits) - move credits, same as the
  dashboard's Viewers page. Revoking only ever touches *unused* credits.

`{user}` takes `<platform>:<id>` (`twitch:123456789`, `kick:123`), a bare
numeric id, or the viewer's name (login or display name, case-insensitive,
on either platform) - so all of these grant GigaChad42 three credits:

```bash
curl -X POST https://api.tippage.com/v1/viewers/twitch:123456789/credits/grant \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" -d '{"amount": 3}'

curl -X POST https://api.tippage.com/v1/viewers/123456789/credits/grant \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" -d '{"amount": 3}'

curl -X POST https://api.tippage.com/v1/viewers/GigaChad42/credits/grant \
  -H "Authorization: Bearer tp_live_..." \
  -H "Content-Type: application/json" -d '{"amount": 3}'
```

Prefer `platform:id` in automation - it survives renames and can never be
ambiguous. A bare id is tried on Twitch then Kick and answers
`409 ambiguous_viewer` if an account with that id signed in on both; a name
shared by two different people is 409 too (a name that matches both of one
person's linked accounts just resolves to the one they signed in with most
recently). A grant or revoke lands on the account you named; balances and
history are read across the person's linked accounts either way.

One rule to design around: **a viewer exists only once they've signed in to
the tip page**. Granting (or looking up) anyone who never has answers `404`
with code `viewer_not_signed_in` and grants nothing - there's no account to
attach the credits to yet. Have the viewer sign in first, then grant.
(Gift-sub credits from Twitch events are the one exception: those wait in a
pending pool and attach automatically on first sign-in - you'll see them as
`credits.pending` on the viewer detail.)

## AI voices

Accounts in the AI voices closed beta can let viewers have their tip read
out in AI voices - one voice for the whole message, or a script where
different lines are spoken by different voices (up to five in one message,
performed as a single take). Voices are TipPage's own,
cloned by the streamer from their own recordings, or published by another
streamer. The `ai_voices:manage` scope exposes the feature to automation:

- [`GET /v1/ai-voices`](/api/reference/ai-voices/listaivoices) - the feature's state
  (on/off, mode, minimum tip), every voice the account can offer with
  per-voice `enabled` flags, a `source` (`platform`/`own`/`shared`) and
  sample clips, plus this month's generation figures. Those figures are
  informational - there is no monthly allowance.
- [`POST /v1/ai-voices/enable`](/api/reference/ai-voices/enableaivoices) /
  [`disable`](/api/reference/ai-voices/disableaivoices) - the master switch, same as
  the dashboard toggle. Handy for turning AI voices on only while live.
- `POST /v1/ai-voices/{voiceId}/enable` / `disable` - curate which voices
  viewers can pick, by the `id`s from the catalog. The last enabled voice
  can't be disabled - use the master switch instead.

Cloning and publishing voices is dashboard-only; there is deliberately no
/v1 surface for uploading someone's voice.

Every `/v1/ai-voices` endpoint answers `404` for accounts without the
feature - the scope won't appear in your dashboard's key editor either.

## A worked example: react to tips from your own server

1. Create a key with `tts:read` + `tts:control`.
2. Add a webhook endpoint subscribed to `tip.created` - or skip webhooks and
   poll [`GET /v1/tts/queue`](/api/reference/tts-queue/getttsqueue) every few seconds.
3. When a tip arrives, claim it with
   [`POST /v1/tts/{orderId}/start`](/api/reference/tts-queue/startttsitem) - the same
   call the overlay makes, so the dashboard shows it as playing and nothing
   else can double-play it.
4. Do your thing - light the lights, post to Discord, play the audio from
   `tts_url` on your own device.
5. Finish with [`POST /v1/tts/{orderId}/finish`](/api/reference/tts-queue/finishttsitem)
   to promote it to history and free the queue for the next tip.

If your integration should *be* the alert experience, turn off the overlay's
TTS so the queue has one consumer. The full protocol - the gates, crash
recovery, and the do's and don'ts - is in
[Claim, do your thing, finish](/api/claim-and-finish).
