RefCampaignDocs

Webhooks

Receive signed, real-time notifications when conversions are created, refunded, paid, or disputed.

Postback and the SDK push data into RefCampaign. Webhooks are the reverse: RefCampaign POSTs a signed JSON event to your server the moment a conversion lifecycle event happens — so you can sync a CRM, trigger a Slack or Zapier automation, or feed a data pipeline without polling.

Webhooks are available from the Growth plan.

Enable a webhook

In the dashboard, go to Settings → Webhooks → Add webhook:

  1. Enter your endpoint URL (must be HTTPS).
  2. Pick the events you want to receive.
  3. Save. The signing secret (whsec_…) is shown once — store it now.

You can configure up to 5 webhooks per account, each with its own secret and event subscriptions. Use Send test to fire a one-off webhook.test ping and confirm your endpoint is reachable, and Rotate secret to issue a new signing secret.

Events

EventFires when
conversion.createdA conversion is recorded.
conversion.refundedA conversion is refunded.
conversion.commission_paidA commission is marked as paid.
conversion.disputedA conversion's payment is disputed.
conversion.dispute_resolvedA payment dispute is resolved.

Payload

Every delivery is a POST with a JSON body of this shape:

{
  "event": "conversion.created",
  "data": { "conversionId": "conv_abc", "amount": 4990, "currency": "EUR" },
  "timestamp": "2026-06-19T12:00:00.000Z",
  "idempotencyKey": "conversion.created:conversion:conv_abc"
}
FieldTypeNotes
eventstringOne of the event types above.
dataobjectEvent-specific payload.
timestampstringISO 8601 emission time.
idempotencyKeystringStable per (event, resource). Use it to dedupe — the same lifecycle event always carries the same key, even across retries.

Verify the signature

Every request carries an X-RefCampaign-Signature header, using the same scheme as Stripe:

X-RefCampaign-Signature: t=<unix_seconds>,v1=<hex_sha256>

The signature is HMAC-SHA256(secret, "<t>.<rawBody>"). Always verify it on the raw request body, before parsing JSON — a re-serialized body will not match.

Verify before you trust

An unverified webhook can be forged by anyone who learns your URL. Reject any request whose signature does not validate, and any whose timestamp is outside a tolerance window (5 minutes is the default) to block replays.

On JS/TS stacks, the SDK ships a helper:

import { verifyWebhookSignature, WEBHOOK_SIGNATURE_HEADER } from '@refcampaign/sdk'

// Express — note express.raw() to keep the body unparsed
app.post('/webhooks/refcampaign', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebhookSignature({
    secret: process.env.REFCAMPAIGN_WEBHOOK_SECRET,
    payload: req.body.toString('utf8'),
    header: req.header(WEBHOOK_SIGNATURE_HEADER) ?? '',
  })
  if (!ok) return res.status(400).send('invalid signature')

  const event = JSON.parse(req.body.toString('utf8'))
  switch (event.event) {
    case 'conversion.created':
      // …
      break
  }
  res.sendStatus(200)
})

On other stacks, recompute the HMAC yourself:

import hmac, hashlib, time

def verify(secret: str, raw_body: str, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    ts, sig = int(parts['t']), parts['v1']
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f'{ts}.{raw_body}'.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Retries and idempotency

  • Respond with a 2xx status as soon as you've accepted the event. Any non-2xx (or a timeout past 10s) is treated as a failure.
  • Failed deliveries are retried with exponential backoff (up to 5 attempts).
  • Deliveries are idempotent on idempotencyKey — a retry never produces a second successful delivery, and your handler should treat a repeated key as a no-op.
  • Keep your handler fast: do the heavy work asynchronously and return 2xx immediately.

Every delivery (and its response status) is recorded in Settings → Webhooks → View deliveries for audit and debugging.

Security

  • Serve your endpoint over HTTPS only.
  • Store the signing secret in a secret manager — never in source code.
  • Verify the signature on every request, on the raw body, with a constant-time comparison.
  • Rotate the secret if it leaks — the previous secret stops validating immediately.

On this page