Copy this prompt to your AI agent. It includes the API contract, documentation and instructions to configure your own key securely.
Integrate the DonoLink Developer API into my existing app. Inspect the code first and follow its architecture, styling and conventions. Ask what user-facing feature I want if it is unclear, then implement and test the integration completely.
Before coding, read the current official documentation:
- https://donolink.nl/en/developers (all tabs: quick start, authentication, endpoints, event format, history/live feed, limits/errors and secure integration)
- https://donolink.nl/developers/openapi.json (machine-readable API contract)
- https://donolink.nl/llms-full.txt (additional product context)
The official docs are authoritative. If you cannot retrieve them, say so and ask for the contract; do not invent endpoints or capabilities.
Ask the streamer to create their own API key at https://donolink.nl/dashboard/instellingen#developer → API keys, for example named “My key”. Ask them to supply it through secure input or a secret manager. If only ordinary chat is available, have them set DONOLINK_API_KEY themselves in a local gitignored .env file or deployment secrets; do not ask them to paste the key into chat. Do not read or display secrets unnecessarily. Never use another streamer's key. Meanwhile, build with a placeholder and check configuration without revealing its value.
API contract summary:
- Base URL: https://donolink.nl/api/v1. GET /me checks the owner, scope and expiry; GET /activity reads only their stored activity. Authentication: Authorization: Bearer <DONOLINK_API_KEY>. Fixed activity:read scope. Read only: no payments, writes, webhooks or multi-customer OAuth.
- /activity accepts limit (1–100, default 50), order (asc/desc, default desc), cursor, type, platform, since and until. Only use documented values. since is inclusive and until exclusive; UTC ISO timestamps filter recorded_at/insertion time, not the original donation date.
- Response: data[] with id, type, platform, occurred_at, recorded_at, actor.name, message, value, unit, currency and refunded_amount_cents. Money is in cents; bits/diamonds are not euros. Treat names and messages as untrusted text: escape output, never execute HTML. No emails or payment identifiers are returned.
- Paid donations and imported history are included; platform events exist only where DonoLink has stored them. Import sources are identified by platform. Complete history, fixed retention and exactly-once delivery are not guaranteed.
- Read history using order=asc and follow pagination.next_cursor while has_more is true. Keep filters and order identical: cursors are bound to them and the owner. Persist the cursor only after successful idempotent processing by event.id. Also save the last page's cursor and use it to poll for new records. Preserve the checkpoint on empty pages. Use one fixed since value for the stream; do not move it on every request.
- For a live feed, use one poller per account, for example every 5 seconds once caught up. Share results inside the app. Limits: 120 requests per account and 300 per IP per 60 seconds. Respect Retry-After on 429/503; use timeouts and capped back-off with jitter for network/5xx errors. Stop automatic retries on 401/403 and tell the streamer to check the key/account. Handle invalid cursors explicitly without duplicate processing.
Build securely:
Keep keys server-side or in secure storage for a personal native app. Never put keys in frontend bundles, URLs, localStorage, logs, screenshots, analytics, test fixtures or Git. A public web app must use its own authenticated backend with per-user authorization. Do not use shared response caches. For multiple streamers, isolate each streamer's secrets and data; this API is not an OAuth platform. Explain key rotation and revocation; revocation takes effect on the next request.
Deliver working project code, safe configuration instructions without real secrets, and appropriate tests for authentication, empty results, historical pagination, deduplication, rate limits and outages. Test real API access once the streamer has configured the key securely. Do not make real payments. Clearly distinguish verified behavior from remaining configuration.
Quick start
Use the API from your own server or personal app. All endpoints are read-only and use JSON. The base URL is https://donolink.nl/api/v1.
Go to Settings → Developer → API keys. Name your integration and choose an expiry date.
Copy the key once and store it as DONOLINK_API_KEY in your server secret configuration.
Make your first request. Without filters, you receive up to 50 events, newest first.
Send your key in the Authorization header with every request. A session cookie, query parameter or overlay token does not grant access to this API.
Authorization: Bearer dl_live_YOUR_SECRET_KEY
Choose a validity of 30, 90 or 365 days. You can have up to 10 active keys and create one key per minute. DonoLink stores only a SHA-256 hash; the full key is visible only immediately after creation.
Use a separate key per integration. To rotate, create a new key, update your app, then revoke the old one. A revoked or expired key stops working on the next request.
Every key has the fixed activity:read scope and is bound to your account. There are no write, payment or administration permissions. OAuth for multi-customer apps is not available yet.
Endpoints
GET/api/v1/me
Check which streamer your key belongs to and when it expires. Only the public profile, scope and key metadata are returned.
Read the combined feed of DonoLink donations and stored platform events. The account is determined exclusively from the API key.
Parameter
Description
limit
Events per page: 1 to 100, default 50.
order
desc (default) for newest first, asc for oldest first and continuous polling. Sorted by recorded_at, then internal record ID and source.
type
Optional: donation, follow, subscription, giftsub, cheer or raid. One type per request.
platform
Optional: donolink, twitch, kick, youtube, tiktok or an import source: tipeeestream, streamlabs, streamelements, kofi, csv. One source per request.
since
Optional, inclusive. UTC ISO 8601, for example 2026-09-07T00:00:00Z. Filters recorded_at, the time of storage.
until
Optional, exclusive. Same date format as since. Keep this boundary fixed during a historical export.
cursor
Pass pagination.next_cursor unchanged. The cursor is signed and bound to your account, filters and order. limit may change.
Unknown or repeated parameters return HTTP 400. Do not put an account ID, key or PocketBase filter in the URL.
Event format
Each event has a stable id prefixed with don_ or evt_. Use this id to prevent duplicate processing after a retry.
type
unit
Description
donation
cents
DonoLink donation or a stored platform donation, such as YouTube Super Chat.
follow
none
New follower. value is 0.
subscription
months
Subscription. value contains the stored number of months.
giftsub
subscriptions
Gifted subscriptions. value is the count.
cheer
bits / diamonds
Bits; for TikTok the unit is diamonds. This is not donation revenue.
raid
viewers
Raid. value is the stored viewer count.
actor.name is the stored display name or null. message is text or null. occurred_at is the donation date; for platform events only the storage time is known. recorded_at always contains the storage time and determines pagination. All timestamps are UTC ISO 8601.
For donation, value is the original amount in integer cents and currency is the currency. refunded_amount_cents contains the refunded amount for DonoLink, otherwise null. Donations with status paid are shown, including imported history with the import source as platform. Fully refunded and disputed donations are excluded. This is an activity feed, not a financial ledger.
Platform events are available to the extent DonoLink received and stored them through an active connection. History from before the connection is not fetched from the platform. There is no guaranteed retention period; deleted records cannot be retrieved. Test alerts are not included.
Webhooks
From endpoint to first event
Add a public HTTPS endpoint under Settings, Outgoing webhooks. Choose your events and save the signing secret. Your activity API key and this secret serve separate purposes.
1. Publish an HTTPS route on port 443 with a valid certificate. 2. Add its URL in Settings → Outgoing webhooks and choose events. 3. Save the entire whsec_ secret as a server environment variable. 4. Send a test and check its delivery in the dashboard.
Headers and payload
Content-Type is application/json. DonoLink-Event-Id matches body.id. DonoLink-Timestamp is the Unix time in seconds of this delivery attempt. DonoLink-Attempt starts at 1. DonoLink-Signature contains v1= followed by 64 hex characters. Header names are case-insensitive; use the entire signing secret including whsec_.
donation.created reports a paid donation. donation.refunded includes the cumulative refunded donation amount. donation.chargeback reports a changed dispute with the current donation status. subscription.created is emitted after the first paid monthly contribution. Donor email addresses and payment credentials are never included. test=true identifies a test sent by the streamer.
created_at is the event time in UTC. Amounts are integer cents; currency is eur. data.id is the internal DonoLink record ID. Fields can be absent depending on event type; do not assume one fixed object for all events. subscription_id in subscription.created is the DonoLink subscription ID, not a Stripe ID.
Verify DonoLink-Signature using HMAC-SHA256 over timestamp.event-id.raw-body. Use the original body bytes before parsing JSON. Also verify the DonoLink-Timestamp header with a maximum five-minute difference. The example below accepts only valid, recent signatures.
Persist event.id in your database to safely recognize repeated deliveries. Return a 2xx response promptly after durably storing the event. Ordering is not guaranteed. Network failures, HTTP 408/425/429 and 5xx responses are attempted up to eight times. Other 4xx responses and redirects end delivery. View failed deliveries and retry them in Settings.
The worker checks the queue roughly every 5 seconds. An HTTP attempt times out after 10 seconds. Temporary failures receive up to seven retries after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours respectively. These delays are not delivery guarantees. A manual retry starts a new attempt sequence with the same event ID and payload, but a fresh timestamp and signature.
Runnable Node.js receiver
This example verifies the raw body, persists unique events in SQLite and only then returns 204. Run Node.js 22.13 or newer, set DONOLINK_WEBHOOK_SECRET and put the server behind your HTTPS proxy. Process the inbox with your own worker; do not grant real rewards or perform real actions for test=true.
401/403: check your signing secret, clock and raw body, then retry manually. 404: verify the route. 3xx: use the final URL; redirects are not followed. 429/5xx: inspect capacity and logs; DonoLink retries. dns_failed/timeout/network_error: check DNS, TLS and connectivity. unsafe_target: use a public HTTPS address; localhost, private IPs and mixed public/private DNS answers are rejected. If a test remains queued, check that the webhook worker is running.
For an export, use order=asc with fixed since and until boundaries. Process data and request the next page using next_cursor while has_more is true. There is no limit on historical page count, but request limits apply.
An empty page preserves your existing cursor. With no previous cursor and no events, next_cursor is null. Cursors contain no API key, but treat them as opaque values. After changing filters, start without a cursor.
For your IRL app, use order=asc, a fixed since and no until. Save the cursor only after processing events. Poll about every 5 seconds; when has_more is true, continue directly to the next page. v1 has no WebSocket or webhook endpoint.
// Node.js: run on your server. Keep the key out of frontend bundles.
const base = new URL('https://donolink.nl/api/v1/activity');
base.searchParams.set('order', 'asc');
base.searchParams.set('limit', '100');
// Persist both this fixed starting point and the returned cursor.
base.searchParams.set('since', '2026-09-07T00:00:00Z');
let cursor = await loadCheckpoint();
for (;;) {
const url = new URL(base);
if (cursor) url.searchParams.set('cursor', cursor);
const response = await fetch(url, {
headers: { Authorization: 'Bearer ' + process.env.DONOLINK_API_KEY },
signal: AbortSignal.timeout(15000)
}).catch(() => null);
if (!response || response.status === 429 || response.status >= 500) {
const seconds = Number(response?.headers.get('Retry-After') || 60);
await new Promise(r => setTimeout(r, seconds * 1000));
continue;
}
if (!response.ok) throw new Error('DonoLink API: ' + response.status);
const page = await response.json();
// Implement these storage functions in your app.
// Process idempotently by event.id, then save the checkpoint.
for (const event of page.data) await processOnce(event.id, event);
if (page.pagination.next_cursor) {
cursor = page.pagination.next_cursor;
await saveCheckpoint(cursor);
}
if (!page.pagination.has_more)
await new Promise(r => setTimeout(r, 5000));
}
The feed reads current stored records and is not an immutable snapshot or a guaranteed exactly-once event bus. Payment status changes can remove previously visible records. Use idempotent processing, retries and, when needed, overlapping reads for recovery.
Limits & errors
Maximum 120 requests per 60 seconds per account, shared by all keys. There is also a limit of 300 requests per 60 seconds per IP address. HTTP 429 and 503 include Retry-After: 60. Wait that long and retry with the same cursor.
HTTP
code
Description
400
invalid_parameter / invalid_cursor
Correct your parameters or restart without the invalid cursor.
401
invalid_api_key
Key is missing, invalid, expired or revoked.
403
account_unavailable
The account is unavailable or not verified.
429
rate_limit_exceeded
Too many requests. Respect Retry-After.
503
temporarily_unavailable
Temporary outage. Access is also denied if the shared limiter is unavailable.
{
"error": {
"code": "invalid_api_key",
"message": "API key is invalid, expired or revoked.",
"request_id": "request-uuid"
}
}
Every response has X-Request-Id; error responses also include error.request_id. Include this ID and the time when contacting support. Never share your key, Authorization header or full request logs. Use GET for data. HEAD and OPTIONS are available for HTTP checks; write methods return 405.
Secure integration
Use HTTPS. Never put keys in URLs, Git, browser code, localStorage, analytics or log files. API responses use Cache-Control: private, no-store. The API does not provide cross-origin browser access.
In a personal native app, enter your own key and store it in Keychain or Android Keystore. For apps serving others, use a backend with isolated secrets per user. Never embed a shared key in the app binary.
The API returns only display names, messages, event values and limited profile details. Email addresses, payment methods, Stripe IDs, platform tokens and administration settings are not shared. Treat names and messages as personal data and store only what your integration needs.
If a key leaks, revoke it immediately under Settings → Developer and create a replacement. Revoked and expired keys, blocked accounts and deleted accounts cannot access the API.
Names and messages are untrusted input. Render them as text, never as HTML or executable code. Do not automate payment actions based on this feed.