Integrate Ninja Backer tips into your applications, overlays, and automation tools.
Configure a webhook URL in your dashboard to receive HTTP POST requests when tips arrive.
{
"type": "tip",
"amount": 5.00,
"currency": "USD",
"name": "TipperName",
"message": "Great stream!",
"timestamp": 1702138446000
}
| Field | Type | Description |
|---|---|---|
| type | string | Always "tip" |
| amount | number | Amount in the stated currency: 5.00 USD or 500 JPY (not minor units) |
| currency | string | Currency code (USD, EUR, GBP, etc.) |
| name | string | Tipper's display name |
| message | string | Optional message from tipper |
| timestamp | number | Notification time in milliseconds; unchanged on retry |
| isTest | boolean | Present and true for simulated tips; never fulfill paid rewards for these |
| callbackId | string | Optional caller-supplied correlation value; not proof of payment |
| anonymous | boolean | Optional anonymity preference; respect it when displaying the tip |
Delivery failures retry automatically for up to seven days. Deduplicate the
X-NinjaBacker-Delivery header: retries retain the same ID and payload.
Destinations must be public HTTPS URLs without embedded credentials. Private/loopback addresses and private DNS answers are blocked.
Up to three HTTPS redirects are allowed, with every destination checked. Each attempt has a five-second total deadline.
Return a 2xx response after durably accepting the notification. Retry delays start at one minute and cap at one hour.
Changing your endpoint stops retries to the previous URL.
For added security, you can enable HMAC-SHA256 signatures on outgoing webhooks. When enabled, each webhook includes a signature header:
X-NinjaBacker-Signature: t=1703721600,v1=abc123def456...
Go to your Dashboard, expand the Webhook section, and click Generate to create a webhook secret.
Store this secret securely — you'll need it to verify incoming webhooks.
Verify the exact raw request bytes before parsing JSON. Once signatures are enabled, reject missing or malformed signatures too.
Check the header timestamp, not the payload timestamp: delayed retries retain their original payload and get a fresh signature.
The example requires Express and your own durable persistTipOnce implementation; it is not a complete receiver.
const crypto = require('node:crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
if (!Buffer.isBuffer(rawBody) || typeof signatureHeader !== 'string' || !secret) return false;
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signatureHeader);
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp) || Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(match[1] + '.').update(rawBody).digest();
return crypto.timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}
// Express example: mount before any express.json() middleware.
const express = require('express');
const app = express();
const secret = process.env.NINJABACKER_WEBHOOK_SECRET;
if (!secret) throw new Error('Configure NINJABACKER_WEBHOOK_SECRET');
app.post('/webhook', express.raw({ type: 'application/json', limit: '32kb' }), async (req, res) => {
if (!verifyWebhook(req.body, req.headers['x-ninjabacker-signature'], secret)) {
return res.status(401).send('Invalid signature');
}
let tip;
try { tip = JSON.parse(req.body.toString('utf8')); }
catch { return res.status(400).send('Invalid JSON'); }
if (tip.type !== 'tip' || tip.isTest === true) return res.sendStatus(204);
const deliveryId = req.headers['x-ninjabacker-delivery'];
if (typeof deliveryId !== 'string' || !deliveryId) return res.sendStatus(400);
try {
// Implement this with your database/queue: atomically record deliveryId
// under a unique constraint and enqueue the tip, ignoring duplicates.
await persistTipOnce(deliveryId, tip);
return res.sendStatus(204);
} catch {
return res.sendStatus(503); // NinjaBacker will retry; do not acknowledge lost work.
}
});
For real-time integrations, connect using the private Tip ID from your dashboard, not your public username.
Keep this token and overlay URLs out of public viewer links. EventSource reconnects automatically, but SSE has no replay or Last-Event-ID recovery.
Use webhooks for durable automation; the first SSE message has type: "connected".
GET https://ninjabacker.com/v1/subscribe/{YOUR_TIP_ID}
const tipId = 'your_tip_id_here';
const events = new EventSource(`https://ninjabacker.com/v1/subscribe/${tipId}`);
events.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'tip') {
if (data.isTest) return; // Ignore simulated tips for paid automation.
console.log(data.currency + ' ' + data.amount + ' from ' + data.fromLabel);
console.log(`Message: ${data.message}`);
}
};
events.onerror = () => {
console.log('Connection lost, reconnecting...');
};
{
"type": "tip",
"amount": 5.00,
"currency": "USD",
"fromLabel": "TipperName",
"message": "Great stream!",
"timestamp": 1702138446000
}
Add the &tip=YOUR_TIP_ID parameter to your VDO.Ninja URL to enable in-stream tipping.
https://vdo.ninja/?push=streamid&tip=YOUR_TIP_ID
| Parameter | Description | Example |
|---|---|---|
&tip=ID | Your Tip ID from dashboard | &tip=aS_WnIp8IPrV |
&tip | Show setup modal (no ID) | &tip |
&tipqrsize=N | QR code size in pixels | &tipqrsize=200 |
¬ipqr | Hide the viewer QR overlay | ¬ipqr |
&tipamounts=A,B,C | Initial preset amounts; performer metadata takes precedence | &tipamounts=5,10,25,50 |
&tipcurrency=XXX | Initial display currency; the performer controls charge currency | &tipcurrency=EUR |
Viewers must opt-in to see tip UI. Add &showtips to viewer URLs:
https://vdo.ninja/?view=streamid&showtips
If you prefer not to show the QR code overlay on your video:
https://vdo.ninja/?view=streamid&showtips¬ipqr
Adjust the QR code size (default is 150px):
https://vdo.ninja/?push=streamid&tip=YOUR_TIP_ID&tipqrsize=100
For most integrations, link to https://ninjabacker.com/USERNAME?callback=YOUR_REFERENCE.
The reference returns as callbackId; choose an opaque identifier, never a password or session token.
Custom checkouts can read GET /v1/public/performer/USERNAME, then create a payment using
POST /v1/tip/intent with performerUsername, amount (major units), and currency.
Initialize Stripe.js with the returned stripePublishableKey and stripeAccountId as its connected-account context.
Compare the intent's amount and currency with the amount the viewer approved before confirming its client secret.
After Stripe succeeds, call POST /v1/tip/confirm with tipId and paymentIntentId.
If notification confirmation fails, retain those IDs and retry that request; never create another payment for the same successful charge.
A currency mismatch returns HTTP 409; show the new currency and require another explicit Send Tip action.
Supported currencies: USD, EUR, GBP, CAD, AUD, JPY. Bounds are 1?1000 major units for currencies with cents and 50?1000 JPY.
GET /v1/tips/TIP_ID history uses minor units in amount; use display_amount for display.
Webhooks and SSE already use major units. The legacy performer endpoint no longer exposes private Tip IDs by default.
To use with Social Stream, enter your Social Stream webhook URL in the dashboard. Tips will appear alongside your chat messages.
Use your service's actual public HTTPS webhook URL, not its dashboard, browser-source URL, or SSE URL.
For Streamer.bot or another desktop receiver, place an authenticated public HTTPS receiver in front of the local automation.
Handle isTest separately and deduplicate retries before issuing gifts, credits, or other paid actions.
Use the "Send Test Tip" button in your dashboard to trigger a test notification without making a real payment. Test tips include "isTest": true in the payload.
Your profile avatar is automatically loaded from Gravatar using your Stripe account email.
If no Gravatar is found, a default placeholder image is displayed.