Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.wiseyield.co/llms.txt

Use this file to discover all available pages before exploring further.

When WiseYield delivers a webhook to your application, it is signed using the Standard Webhooks specification (the same scheme used by Svix, Resend, Dodo Payments, and Clerk). This guide shows you how to verify the signature and what to expect in the payload.
Outbound webhooks are part of the versioned API expansion roadmap. The signature contract documented here is what integrators should align against today so the wire format does not change at GA.

The contract

WiseYield sends three headers with every webhook delivery:
HeaderFormatPurpose
webhook-idUUIDStable identifier for this delivery (for idempotency)
webhook-timestampUnix secondsWhen the webhook was signed (reject if too old)
webhook-signaturev1,<base64-signature> (space-separated for multiple)HMAC-SHA256 of {id}.{timestamp}.{body} using your endpoint secret
The body is JSON:
{
  "type": "farm.created",
  "id": "evt_...",
  "timestamp": "2026-05-17T14:23:01.000Z",
  "data": { /* event-specific */ }
}

Header fallback (Svix parity)

WiseYield reads webhook-* first and falls back to svix-* on the way in, and emits both header sets on the way out for downstream compatibility. Your handler should do the same:
const id =
  req.headers['webhook-id'] ?? req.headers['svix-id'];
const timestamp =
  req.headers['webhook-timestamp'] ?? req.headers['svix-timestamp'];
const signature =
  req.headers['webhook-signature'] ?? req.headers['svix-signature'];

Verifying the signature

The canonical approach is to use the official standardwebhooks (or svix) library — it handles timing-safe comparison, multi-signature parsing, and replay-window validation correctly.
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.WISEYIELD_WEBHOOK_SECRET);

app.post('/webhooks/wiseyield', express.raw({ type: 'application/json' }), (req, res) => {
  const headers = {
    'webhook-id': req.headers['webhook-id'] ?? req.headers['svix-id'],
    'webhook-timestamp': req.headers['webhook-timestamp'] ?? req.headers['svix-timestamp'],
    'webhook-signature': req.headers['webhook-signature'] ?? req.headers['svix-signature'],
  };

  let event;
  try {
    event = wh.verify(req.body, headers);
  } catch (err) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // event is the parsed, verified JSON payload
  processEvent(event);
  res.status(200).end();
});

Why raw body matters

The signature is computed over the raw request body bytes, not the JSON-parsed object. Most web frameworks parse JSON by default — you must read the raw body before parsing, or signature verification will fail intermittently when whitespace or key ordering differs.
  • Express: use express.raw({ type: 'application/json' }) on the webhook route.
  • Next.js App Router: use request.text() before JSON.parse().
  • Flask: use request.data (not request.json).
  • FastAPI: use await request.body().

Idempotency

Webhooks can be delivered more than once — retries on network blips, processing failures on your end, etc. Use webhook-id as the dedupe key:
async function processEvent(event, webhookId) {
  const seen = await db.webhookDeliveries.upsert({
    where: { id: webhookId },
    update: {},
    create: { id: webhookId, receivedAt: new Date() },
  });
  if (seen.processedAt) {
    return; // Already handled
  }
  await doTheActualWork(event);
  await db.webhookDeliveries.update({
    where: { id: webhookId },
    data: { processedAt: new Date() },
  });
}
A unique constraint on webhook_id plus ON CONFLICT DO NOTHING is the simplest form. WiseYield uses exactly this pattern internally for the webhooks it consumes.

Replay window

webhook-timestamp is signed alongside the body. Reject deliveries older than ~5 minutes to prevent replay attacks:
const ageSeconds = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (Math.abs(ageSeconds) > 300) {
  return res.status(401).json({ error: 'Replay window exceeded' });
}
The official standardwebhooks and svix libraries enforce this automatically.

Retry behavior

When your endpoint returns a non-2xx response (or fails to respond within 30 seconds), WiseYield retries with exponential backoff:
AttemptDelay
1Immediate
25 seconds
330 seconds
45 minutes
530 minutes
62 hours
75 hours
810 hours
924 hours
After 9 failed attempts, the delivery is marked permanently failed and surfaced in your webhook dashboard. Return 200 as soon as you’ve verified the signature and queued the event for processing; do any heavy work asynchronously.

What to log on verification failure

When verification fails, log loudly enough to diagnose:
logger.warn('WiseYield webhook signature verification failed', {
  webhookId: req.headers['webhook-id'] ?? req.headers['svix-id'],
  seenHeaders: {
    'webhook-id': req.headers['webhook-id'],
    'svix-id': req.headers['svix-id'],
    'webhook-timestamp': req.headers['webhook-timestamp'],
    'svix-timestamp': req.headers['svix-timestamp'],
  },
  bodyLength: req.body.length,
});
The most common causes of legitimate-signature-but-failing-verification:
  • Body was JSON-parsed before signature check (always read raw bytes first).
  • Wrong endpoint secret (rotate, store in env, never commit).
  • Reverse-proxy stripping or re-casing headers (check for Webhook-Id vs webhook-id).
  • Reading the wrong header set (you sent svix-*, your code only looks for webhook-*, or vice versa).

See also