Skip to main content
Crovver can push subscription lifecycle events to HTTP endpoints you register. When a subscription is created, activated, renewed, or canceled — whether triggered manually from your dashboard or by a payment provider — Crovver signs a JSON payload and POSTs it to every active endpoint you’ve configured.

Setting Up an Endpoint

  1. Go to Developers → Webhooks in your Crovver dashboard
  2. Click + Add Endpoint
  3. Enter your HTTPS URL — e.g. https://api.your-app.com/webhooks/crovver
  4. Copy the signing secret shown — it is displayed only once and cannot be retrieved again
Your endpoint must respond with a 2xx status within 5 seconds. Any other response or a timeout is recorded as a failed delivery.

Event Envelope

Every event shares the same top-level structure:
{
  "id": "a1b2c3d4-e5f6-...",
  "type": "subscription.activated",
  "created_at": "2026-06-07T10:00:00.000Z",
  "org_id": "org_...",
  "data": {
    "id": "sub_...",
    "status": "active",
    "plan_id": "plan_...",
    "billing_mode": "recurring",
    "external_tenant_id": "your_user_id_123",
    "tenant_name": "Acme Corp",
    "current_period_start": "2026-06-07T00:00:00Z",
    "current_period_end": "2026-07-07T00:00:00Z",
    "capacity_units": 5,
    "created_at": "2026-06-01T00:00:00Z",
    "updated_at": "2026-06-07T10:00:00Z"
  }
}
data.external_tenant_id is the ID you provided when creating the tenant — use this to identify the user or workspace in your own system. data.tenant_name is the human-readable name for quick identification.

Events Reference

EventWhen it fires
subscription.createdA subscription is manually created from the dashboard
subscription.activatedA subscription goes live for the first time — checkout completes or an invoice is marked paid
subscription.updatedStatus, billing period, or seat count changes
subscription.renewedA recurring billing cycle payment succeeds and the period advances
subscription.canceledSubscription is canceled by an admin, the tenant, or the payment provider
webhook.testSent when you click Send Test from the dashboard
When a free trial ends and the first charge succeeds, both subscription.updated (status: trialing → active) and subscription.renewed (payment received) fire in sequence. These represent distinct state changes — handle them independently and use the top-level id to deduplicate if needed.

Verifying Signatures

Every request includes an X-Crovver-Signature header. Always verify it before processing the event.
X-Crovver-Signature: sha256=<hmac-hex>
X-Crovver-Event: subscription.activated
User-Agent: Crovver-Webhook/1.0
The signature is HMAC-SHA256 of the raw request body using your endpoint’s signing secret.
import crypto from "crypto";
import express from "express";

const app = express();

// Use raw body middleware — do NOT parse JSON before verifying
app.post(
  "/webhooks/crovver",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.headers["x-crovver-signature"] as string;

    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", process.env.WEBHOOK_SECRET!)
        .update(req.body) // raw Buffer — not re-serialized JSON
        .digest("hex");

    const valid =
      sig?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));

    if (!valid) {
      return res.status(401).json({ error: "Invalid signature" });
    }

    const event = JSON.parse(req.body.toString());

    switch (event.type) {
      case "subscription.activated":
        // Grant access for event.data.external_tenant_id
        break;
      case "subscription.renewed":
        // Extend access period
        break;
      case "subscription.canceled":
        // Revoke access
        break;
    }

    // Always respond 200 quickly
    res.status(200).json({ received: true });
  }
);
Always verify the signature against the raw request body bytes — not a re-serialized version. JSON parsers may reorder keys, which will break the HMAC comparison.

Delivery & Retries

Crovver delivers each event once. If your endpoint is down or returns a non-2xx status, the delivery is recorded as failed. In Developers → Webhooks, click View Deliveries on any endpoint to see:
  • The full JSON payload that was sent
  • The HTTP status and response body your server returned
  • Attempt count and timestamp
Click Retry on any failed delivery to re-fire it with the same id — so your server can safely deduplicate.
Use the top-level id field as an idempotency key. Store processed event IDs and skip duplicates to handle retries safely.

Testing Locally

Use the Send Test button in your dashboard to fire a webhook.test event to any registered endpoint. The delivery and its payload will appear in the View Deliveries log immediately. To receive events on your local machine, expose it with a tunneling tool like ngrok:
ngrok http 3000
# → https://abc123.ngrok-free.app
Register the generated HTTPS URL as your endpoint — e.g. https://abc123.ngrok-free.app/webhooks/crovver — then use Send Test to verify your handler end-to-end before deploying.

Security Notes

ConcernHow it’s handled
Signature forgeryHMAC-SHA256 per-endpoint secret — compare with timingSafeEqual
Secret exposureSigning secret shown once at creation, never returned again
Sensitive fieldsProvider IDs, admin-only fields, and internal credentials are stripped from all payloads
Delivery errorsNever surface to the API caller — your subscription action always completes regardless