Skip to content

Signed webhooks: know everything instantly

Adding an endpoint, choosing events, verifying the signature in Node.js, Python and PHP, retries, idempotency and the full event list.

10 min read4 chaptersFor foundersAdvancedUpdated Oct 17, 2026

Chapter 1Adding an endpoint

  1. In API and webhooks, tap “Add endpoint”
  2. Type your server URLIt must be https and public (no localhost or internal IPs). Redirects are not followed.
  3. Choose events“All events”, or the ones you want. A group wildcard such as milestone.* works too.
  4. Copy the signing secretStore it as KROWS_WEBHOOK_SECRET.
  5. Tap “Send test event”A test.ping arrives. Check the result in the log.

Each endpoint belongs to one mode: test or live, per the selector.

Chapter 2Verifying the signature

Each delivery is a POST with Content-Type: application/json, User-Agent: Krows-Webhooks/1.0 and the Krows-Signature: t=<timestamp>,v1=<signature> header. The signature is the hex HMAC-SHA256 of t + "." + body with your endpoint secret. Use the raw body exactly as received.

import crypto from 'node:crypto';
import express from 'express';
const app = express();

// Use the raw body: the signature is computed over the exact bytes we send
app.post('/webhooks/krows', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Krows-Signature') ?? '';
  const { t, v1 } = Object.fromEntries(header.split(',').map((x) => x.split('=')));
  const expected = crypto.createHmac('sha256', process.env.KROWS_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest('hex');
  const valid = v1 && v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  // Reject anything older than 5 minutes to stop replays
  if (!valid || Date.now() / 1000 - Number(t) > 300) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  switch (event.type) {
    case 'investment.confirmed':
      // event.data is the investment: amount, tokens, investor, reference, metadata…
      break;
    case 'round.succeeded':
      break;
  }
  res.sendStatus(200); // answer 2xx within 10 seconds
});

Chapter 3Responses and retries

  • Reply 2xx within 10 seconds. If you have long work, queue it and reply right away.
  • On failure we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours: up to 6 retries.
  • The same event can arrive more than once: store the event id and process it once.
  • Order is not guaranteed: use created and query the API if needed.
  • If you missed something, GET /v1/events returns the history.

Chapter 4Every event

EventWhen
investment.confirmedSomeone invested
investment.withdrawnAn investor withdrew
checkout.session.completedAn API-created session completes
round.succeededThe round reaches 100%
round.failedThe round fails or is cancelled
tokens.deliveredAn investor receives tokens
refund.completedUSDC refunded to an investor
milestone.requestedYou request a milestone and a vote opens
milestone.approvedBackers approve a milestone and funds are released
milestone.rejectedBackers reject a milestone
report.deliveredYou deliver a report
referendum.openedA vote to close the startup opens
referendum.closedThe closing vote ends
free_withdrawal.requestedYou request a free withdrawal and the veto period starts
free_withdrawal.paidNobody vetoes it and the contract pays you
free_withdrawal.vetoedBackers veto it
test.pingTest event from the dashboard
Was this helpful?