Skip to main content
Webhooks let your application react to events as they happen — a payment succeeds, a refund is created, a customer is updated. Instead of polling the API, you register a URL and we send you an HTTP POST with the event data.

Setting up a webhook endpoint

Create an endpoint by specifying a URL and the event types you want to receive:
curl -X POST https://api.leanrails.com/v1/webhook_endpoints \
  -u "$API_KEY:" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks",
    "enabled_events": [
      "payment_intent.succeeded",
      "payment_intent.payment_failed",
      "charge.refunded"
    ]
  }'
The webhook secret (whsec_...) is only returned once — when you create the endpoint. Store it securely. You’ll need it to verify webhook signatures.

Verifying signatures

Every webhook delivery includes an X-Signature header. Always verify this signature before processing the event to ensure it came from us and hasn’t been tampered with. The signature header format is:
X-Signature: t=1709913600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The signature is computed as:
HMAC-SHA256(webhook_secret, "v1=" + timestamp + "." + raw_request_body)
const crypto = require("crypto");

function verifyWebhookSignature(payload, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((item) => item.split("=", 2))
  );
  const timestamp = parts.t;
  const expectedSig = parts.v1;

  // Reject timestamps older than 5 minutes to prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error("Timestamp too old");
  }

  const signedPayload = `v1=${timestamp}.${payload}`;
  const computedSig = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  if (
    !crypto.timingSafeEqual(
      Buffer.from(computedSig),
      Buffer.from(expectedSig)
    )
  ) {
    throw new Error("Invalid signature");
  }

  return JSON.parse(payload);
}

// Express.js example
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-signature"];
  const event = verifyWebhookSignature(req.body.toString(), sig, WEBHOOK_SECRET);

  switch (event.type) {
    case "payment_intent.succeeded":
      console.log("Payment succeeded:", event.data.object.id);
      break;
    case "payment_intent.payment_failed":
      console.log("Payment failed:", event.data.object.id);
      break;
  }

  res.status(200).json({ received: true });
});

Retry schedule

If your endpoint returns a non-2xx response or times out, we retry the delivery with increasing delays:
AttemptDelayCumulative Time
1Immediate0
21 minute1 minute
35 minutes6 minutes
430 minutes36 minutes
52 hours~2.5 hours
68 hours~10.5 hours
724 hours~1.5 days
872 hours~4.5 days
After 8 failed attempts, the delivery is marked as exhausted and no further retries are attempted.

Endpoint requirements

  • Must accept POST requests with Content-Type: application/json
  • Must return a 2xx status code within 30 seconds
  • Must verify the X-Signature header before processing

Best practices

Respond quickly. Return a 200 response immediately, then process the event asynchronously. If your handler takes longer than 30 seconds, the delivery will be marked as failed and retried.
  • Verify signatures on every request to prevent spoofing
  • Deduplicate by event ID — the same event may be delivered more than once during retries
  • Process asynchronously — use a message queue to handle events outside the request cycle
  • Subscribe selectively — only subscribe to event types you need to reduce load
  • Handle test events — in test mode, events include "livemode": false

Event types

See Event Types for the complete list of events you can subscribe to.