List donations
Read the donation stickers received during the current broadcast. The donation event on the stream arrives in real time only once, so use this API to recover donations that arrived while your bot was down.
| Method and path | GET /v1/live/donations |
| Permission | donations.read |
| Success | 200 |
| When not on air | 404 (OAPI_MNGR_0301) |
A broadcast with many donations does not come back at once — you follow a cursor.
export SPOON_BASE_URL="https://kr-openapi.spooncast.net"
export SPOON_ACCESS_TOKEN="{your access token}"
For the Japan region use https://jp-openapi.spooncast.net — see regions.
Request
GET /v1/live/donations?cursor={cursor}
Headers
| Name | Required | Description |
|---|---|---|
Authorization | ✔ | Bearer {access_token}. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
cursor | string | Pass nextCursor from the previous response. Omit it for the first page. |
Request example
# First page
curl -H "Authorization: Bearer $SPOON_ACCESS_TOKEN" "$SPOON_BASE_URL/v1/live/donations"
# Next page — pass the nextCursor from the previous response
curl -H "Authorization: Bearer $SPOON_ACCESS_TOKEN" \
"$SPOON_BASE_URL/v1/live/donations?cursor=1024307"
Response
{
"donations": [
{
"user": {
"id": "Ab1Cd2Ef3Gh4Ij5Kl6Mn7O",
"nickname": "SpoonListener"
},
"stickerName": "Heart",
"amount": 300,
"time": "2026-10-19T05:47:50.512Z"
}
],
"nextCursor": "1024307"
}
| Field | Type | Nullable | Description |
|---|---|---|---|
donations | object[] | No | The donations received during the current broadcast. |
donations[].user | object | No | The donor. |
donations[].user.id | string | No | Identifies the donor. An encrypted value. |
donations[].user.nickname | string | No | The nickname. |
donations[].stickerName | string | No | The display name of the sticker, exactly as it appears in the app. |
donations[].amount | number | No | The total spoons of this donation. The same value as amount on the donation event. |
donations[].time | string | No | When the donation arrived. ISO-8601 UTC (Z). |
nextCursor | string | Yes | The cursor for the next page. null means this is the last page. |
user.iduser.id is this person's unique identifier. The same person always gets the same value, so
use it as the key for attendance, running totals, and rankings. Do not identify people by
user.nickname — it changes and it repeats.
The value is scoped to your bot and that DJ. It survives a DJ disconnecting and granting consent again.
This user is the same shape and the same id as user on the event stream, so you can
merge this history with the donations you collected in real time under the same key.
Code
// donations.ts — node --experimental-strip-types donations.ts
interface Donation {
user: { id: string; nickname: string };
stickerName: string;
amount: number;
time: string;
}
interface DonationsPage {
donations: Donation[];
nextCursor: string | null;
}
const BASE = process.env.SPOON_BASE_URL;
const TOKEN = process.env.SPOON_ACCESS_TOKEN;
const all = [];
let cursor;
do {
const url = new URL(`${BASE}/v1/live/donations`);
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!res.ok) throw new Error(`Could not list donations: ${res.status}`);
const page = (await res.json()) as DonationsPage;
all.push(...page.donations);
cursor = page.nextCursor; // null ends the loop
} while (cursor);
const total = all.reduce((sum, d) => sum + d.amount, 0);
console.log(`${all.length} donations this broadcast, ${total} spoons in total.`);
// Recovery after a restart — reprocess only what came after the last handled time
const lastProcessedTime = '2026-10-19T05:00:00.000Z';
const missed = all.filter((d) => d.time > lastProcessedTime);
console.log(`Reprocessing ${missed.length} missed donations.`);
node --experimental-strip-types donations.ts
Watch out
It covers the current broadcast only. Once the broadcast ends its history is gone, and the next broadcast returns only its own donations. To keep totals across broadcasts, store them yourself.
There is no message field. The message sent along with a donation is carried only by the donation event. If you need it, collect it in real time and store it.
Use the event stream day to day. This API is for recovery — after reconnecting, pull the
history to fill the gap, and when the same donation shows up on both sides, deduplicate by
the combination of user.id and time.
Loop until nextCursor is null. Taking only the first page means seeing part of the
history on a generous broadcast. Conversely, do not pass null back as ?cursor= — it is
the signal to stop.
Errors
| HTTP | detailCode | What happened | What to do |
|---|---|---|---|
400 | OAPI_MNGR_0103 | The cursor is invalid. It is not a nextCursor from a previous response. | Start over from the first page without cursor. |
401 | none | The token is missing or invalid. | Ask the DJ to authorize again. |
403 | none | The donations.read permission is missing. | Add the permission and get consent again. |
404 | OAPI_MNGR_0301 | The DJ is not on air. | A state, not an error. Call again shortly. |
502 | OAPI_MNGR_2302 | Broadcast server error. | Retry after a backoff. |
States outside this table can arrive too. A wrong method or Content-Type
(400, 405, 415 — OAPI_MNGR_0103), the call quota (429, no body), and server errors
(500 — OAPI_MNGR_1002) are common to every endpoint — see
Error codes.
Related documents
- Endpoint list — the full endpoint list.
- Event stream · donation — real-time donations with the message.
- Read the fan ranking — running donation totals of the people in the room right now.
- Error codes — the full list of error codes.