LinkJoltDevelopers

Manage API Keys

← Back to LinkJolt.io

← All guides

Set up outbound webhooks

Receive real-time events when conversions, payouts, or affiliates change — no polling required.

Intermediate

10 min read

Ultimate plan

Available events

  • conversion.created — new sale recorded
  • conversion.approved — merchant approved a conversion
  • conversion.rejected — merchant rejected a conversion
  • conversion.paid — commission payout completed
  • conversion.updated — any other status change
  • affiliate.joined — affiliate joined your campaign
  • affiliate.approved — you approved a pending affiliate
  • affiliate.rejected — you rejected a pending affiliate
  • payout.completed — Stripe Connect payout succeeded
  • payout.failed — Stripe Connect payout failed

Step 1 — Create an endpoint on your server

Your 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');
});

Step 2 — Register the webhook

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...", ... } }

Step 3 — Verify signatures

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')
  );
}

Step 4 — Test with a ping

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": "..." } }

Operational notes

  • Retries: 3 attempts with exponential backoff. After 5 consecutive failures we auto-deactivate the subscription (you'll see active: false).
  • Timeout: 10 seconds per request. Return 2xx fast — do heavy work async.
  • Ordering: not guaranteed. If you need order, use the id field for dedup and sort by server time.
  • Rotate secrets: POST /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