Receive real-time events when conversions, payouts, or affiliates change — no polling required.
10 min read
conversion.created — new sale recordedconversion.approved — merchant approved a conversionconversion.rejected — merchant rejected a conversionconversion.paid — commission payout completedconversion.updated — any other status changeaffiliate.joined — affiliate joined your campaignaffiliate.approved — you approved a pending affiliateaffiliate.rejected — you rejected a pending affiliatepayout.completed — Stripe Connect payout succeededpayout.failed — Stripe Connect payout failedYour URL must be public HTTPS. Private IPs (localhost, 10.x, 192.168.x, etc.) are blocked to prevent SSRF.
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/linkjolt', (req, res) => {
const rawBody = req.body.toString('utf8');
const sigHeader = req.headers['x-linkjolt-signature'] as string;
if (!verify(rawBody, sigHeader, process.env.LINKJOLT_WEBHOOK_SECRET!)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(rawBody);
console.log('got', event.event, event.data);
// ACK quickly (return 2xx within 10s to avoid retries)
res.status(200).send('ok');
});POST to /api/v1/webhooks with your URL and the events you want. The signing secret is returned once — store it safely.
curl https://linkjolt.io/api/v1/webhooks \
-X POST \
-H "Authorization: Bearer lj_pk_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/linkjolt",
"events": ["conversion.created", "conversion.approved", "payout.completed"]
}'
# Response includes the signing secret — copy it NOW:
# { "data": { "id": "wh_abc", "secret": "hex_64_chars...", ... } }Every webhook includes X-LinkJolt-Signature: t=TIMESTAMP,v1=HEX. Verify using HMAC-SHA256 over ${timestamp}.${raw_body} with your secret. Reject if mismatched or timestamp is older than 5 minutes (replay protection).
function verify(rawBody, sigHeader, secret) {
const parts = Object.fromEntries(sigHeader.split(',').map(p => p.split('=')));
const { t: timestamp, v1: signature } = parts;
if (!timestamp || !signature) return false;
// Replay protection: reject if older than 5 min
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (Math.abs(age) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex')
);
}Trigger a test delivery to your endpoint. Returns the HTTP status your URL responded with — use this to debug before production.
curl https://linkjolt.io/api/v1/webhooks/wh_abc/test \
-X POST \
-H "Authorization: Bearer lj_pk_your_key"
# Response tells you if your endpoint accepted the ping:
# { "data": { "success": true, "httpStatus": 200, "responseBody": "ok", "url": "..." } }active: false).id field for dedup and sort by server time./api/v1/webhooks/{id}/rotate returns a new secret. The old one stops working immediately — deploy the new one first.Full reference: Webhook docs · Webhook management API