SubsieBeta
Reference

Webhooks

Subsie pushes a signed event to your server for every meaningful transition — sessions completing, payments settling, subscriptions changing state. Verify the signature, handle the event idempotently, and respond fast; Subsie takes care of retries.

Event types

EventFires whendata.object
checkout.session.completedA checkout session is paid and verified on-chain.checkout.session
payment.succeededA payment settles — one-time or recurring.payment
payment.failedA recurring charge attempt fails.payment
payment.refundedA refund is recorded (partial or full) against a payment.payment
subscription.createdA buyer completes a subscription checkout.subscription
subscription.past_dueA charge failed and dunning retries are scheduled.subscription
subscription.expiredRetries are exhausted and the subscription lapses.subscription
subscription.canceledThe subscriber cancels on-chain or via the portal.subscription

Endpoints are mode-scoped: an endpoint created with a test key receives only test events (livemode: false), a live endpoint only live events. The objects inside data.object are exactly the shapes from the API reference.

The event envelope

Example delivery
POST https://example.com/webhooks/orven
Content-Type: application/json
User-Agent: Orven-Webhooks/1.0
Orven-Signature: t=1781234567,v1=5f8c1f2e9a...

{
  "id": "evt_Xc4nQv8MfYwRtnKpBZjdLh9F",
  "type": "payment.succeeded",
  "created_at": "2026-06-12T10:05:00.000Z",
  "data": {
    "object": {
      "id": "pay_e8aiLUZADe3yCMh5RsAecHV3",
      "object": "payment",
      "livemode": true,
      "status": "confirmed",
      "amount": "0.1",
      "fee_amount": "0.002",
      "merchant_amount": "0.098",
      "...": "..."
    }
  }
}

Verifying signatures

Every delivery carries an Orven-Signature header. The signature is an HMAC-SHA256 over the timestamp and the raw request body, keyed with your endpoint secret (whsec_…, returned once when the endpoint is created):

Signature scheme
Orven-Signature: t=<unix seconds>,v1=<hex>

v1 = HMAC_SHA256(secret, "<t>.<raw body>")

Reject deliveries whose timestamp is outside your tolerance window (default five minutes) and compare signatures in constant time. With the SDK this is one call:

With @orven/node
import express from "express";
import { Orven, OrvenWebhookVerificationError } from "@orven/node";

app.post(
  "/webhooks/orven",
  express.raw({ type: "application/json" }), // keep the raw bytes
  (req, res) => {
    let event;
    try {
      event = Orven.webhooks.constructEvent({
        payload: req.body,
        header: req.get("Orven-Signature") ?? "",
        secret: process.env.ORVEN_WEBHOOK_SECRET!,
        toleranceSeconds: 300, // default
      });
    } catch (error) {
      if (error instanceof OrvenWebhookVerificationError) {
        return res.status(400).send("invalid signature");
      }
      throw error;
    }

    switch (event.type) {
      case "checkout.session.completed":
        // fulfill the order
        break;
      case "subscription.past_due":
        // notify the customer
        break;
    }

    res.status(200).end();
  },
);

Without the SDK, verification is a few lines of Node:

Manual verification
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyOrvenSignature(
  rawBody, // string | Buffer — the bytes exactly as received
  header,  // the Orven-Signature header value
  secret,
  toleranceSeconds = 300,
) {
  const parts = Object.fromEntries(
    header.split(",").map((pair) => pair.split("=", 2)),
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const provided = Buffer.from(parts.v1 ?? "", "hex");
  const wanted = Buffer.from(expected, "hex");
  return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
Raw body required
Verify against the exact bytes Subsie sent. JSON middleware that parses and re-serializes the body (for example a global express.json()) will change the byte sequence and every signature check will fail. Mount a raw-body parser on the webhook route.

Delivery and retries

PropertyBehavior
SuccessAny 2xx response within 10 seconds.
RetriesUp to 5 attempts with exponential backoff (30s base, doubling per attempt).
FailureAfter the 5th failed attempt the event is marked failed and surfaces on the dashboard and the webhook events API.
OrderingNot guaranteed. Use created_at and your own state machine; do not assume events arrive in sequence.
DuplicatesPossible — delivery is at-least-once. Deduplicate by event id.

Recommended handler shape:

Handler checklist
1. Verify the signature before reading anything else.
2. Check event.data.object.livemode matches the environment.
3. Deduplicate by event.id (store processed ids).
4. Acknowledge with 200 immediately; do slow work async.
5. Treat webhooks as notifications — re-fetch via the API
   if you need guaranteed-current state.

Test events and redelivery

You can exercise your handler end to end without making a payment, and replay any delivery after fixing your endpoint. Both ride the normal pipeline — same signature, same retries, same observability.

Send a test event
curl https://pay.example.com/api/v1/webhook_endpoints/we_.../test \
  -H "Authorization: Bearer $ORVEN_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "checkout.session.completed" }'

# or with the SDK:
await orven.webhookEndpoints.sendTest("we_...", { type: "checkout.session.completed" });

The data.object is synthetic but matches the production serializer shapes exactly, with internally consistent fee math and a livemode flag matching the endpoint's mode. Test ids (like cs_test000000000000000000) never resolve through the retrieval APIs, so a handler that re-fetches by id sees a 404 — fulfillment logic keyed to real objects cannot be tricked. Each send gets a fresh envelope id, so repeated tests are not swallowed by your dedupe.

Redeliver an event
curl -X POST https://pay.example.com/api/v1/webhook_events/evt_.../resend \
  -H "Authorization: Bearer $ORVEN_SECRET_KEY"

# or with the SDK:
await orven.webhookEvents.resend("evt_...");

Redelivery creates a fresh delivery row with a full retry budget, carrying the original payload byte-for-byte — including the same envelope id. If your handler deduplicates by event id (it should), both deliveries count as one logical event. The response carries resend_of pointing at the original, whose delivery history stays untouched. Redelivery is refused with 409 while the original or an existing redelivery is still pending— the worker is already retrying it. Both actions are also one click on the dashboard's Webhooks page.

Observability

Every delivery attempt is recorded. The Webhooks page in the dashboard shows recent deliveries per endpoint, and GET /api/v1/webhook_events exposes the same data programmatically — status, attempt count, last response code, last error, and the next retry time. Filter by status=failed to find events your endpoint never accepted.