External donations
Send donations from your platform, like a charity or tipping site, into Fishrift. Each donation plays the creator's alerts, shows in their activity feed, counts towards goals and subathons, and can fire OBS Actions and Streamer.bot.
Getting access
Fishrift sets up each connection for you. Contact Fishrift with the creator's name and your platform's name. You get:
- a source ID, used in the webhook URL
- a shared secret, used to sign requests. It's shown once, so store it safely.
The creator needs a Twitch account connected to Fishrift.
The request
POST https://fishrift.com/webhooks/external-donations/{sourceId}
Content-Type: application/json
X-FishRift-Signature: sha256=<hex HMAC-SHA256 of the body>
X-FishRift-Timestamp: <unix time in seconds>Body
| Field | Required | Type | Notes |
|---|---|---|---|
event_id | Yes | string | Your unique ID for this donation, 1 to 200 characters. Sending the same ID twice is safe |
donor_name | Yes | string | The name to show. Use something like "Anonymous" for anonymous donors |
amount | Yes | string | In whole units, like "5.00". "5,00" also works. No thousands separators |
currency | Yes | string | 3 letters, like EUR |
message | No | string | The donor's message |
Signing
- Make the JSON body as a string.
- Calculate an HMAC-SHA256 of that exact string with your shared secret, as hex.
- Send it as
X-FishRift-Signature: sha256=<hex>. - Send the current unix time in seconds as
X-FishRift-Timestamp. It must be within 5 minutes.
Send the exact bytes you signed. Changing whitespace after signing breaks the signature.
curl
SOURCE_ID="your-source-id"
SECRET="your-shared-secret"
BODY='{"event_id":"don_000123","donor_name":"Anonymous","amount":"5.00","currency":"EUR","message":"Keep it up!"}'
TS=$(date +%s)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST "https://fishrift.com/webhooks/external-donations/$SOURCE_ID" \
-H "Content-Type: application/json" \
-H "X-FishRift-Signature: sha256=$SIG" \
-H "X-FishRift-Timestamp: $TS" \
--data-raw "$BODY"Node.js
import crypto from 'node:crypto';
const body = JSON.stringify({
event_id: 'don_000123',
donor_name: 'Anonymous',
amount: '5.00',
currency: 'EUR',
message: 'Keep it up!',
});
const sig = crypto.createHmac('sha256', process.env.FISHRIFT_SECRET).update(body).digest('hex');
await fetch(`https://fishrift.com/webhooks/external-donations/${process.env.FISHRIFT_SOURCE_ID}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-FishRift-Signature': `sha256=${sig}`,
'X-FishRift-Timestamp': String(Math.floor(Date.now() / 1000)),
},
body,
});Responses
Fishrift answers right away and plays the alert after that.
| Status | Body | Meaning |
|---|---|---|
| 200 | {"status":"ok"} | Accepted |
| 200 | {"status":"already_processed"} | This event_id was sent before. Treat it as success |
| 400 | {"error":"invalid_..."} | Something in the body is wrong. Fix it before sending again |
| 403 | {"error":"bad_signature"} | The signature doesn't match. Check the secret and the exact body |
| 403 | {"error":"timestamp_out_of_window"} | Your clock is off. Send again with a fresh timestamp |
| 404 | {"error":"not_found"} | The source ID doesn't exist or is switched off |
| 429 | Too many requests | More than 1000 requests in a minute |
Retry on network errors and on 5xx responses. Because of event_id, a retry never plays the alert twice.
Currency
Fishrift doesn't convert currencies. Alerts show the amount with the creator's own currency symbol, so send amounts in the creator's currency when you can. The currency you send is kept in the event as raw.currency.
What the creator's widgets receive
{
"id": "ext_src1_don_000123",
"platform": "external",
"type": "external_donation",
"viewer_name": "Anonymous",
"viewer_display_name": "Anonymous",
"amount": 5,
"message": "Keep it up!",
"raw": { "source_name": "Your Platform", "currency": "EUR" },
"timestamp": 1759600000000
}