Webhooks

Receive real-time subscription events at your endpoint.

Overview

Slootly sends webhook events to your endpoint when subscription state changes. Events are signed with HMAC-SHA256 for verification. Failed deliveries are retried with exponential backoff.

Setup

Register a webhook endpoint in the Slootly dashboard. You receive a webhook secret (whsec_...) used to verify event signatures.

import express from 'express';
import { Slootly } from '@slootly/sdk';

const app = express();
const gate = new Slootly('sk_live_...');

// Use raw body for signature verification
app.post(
  '/webhooks/slootly',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const event = gate.verifyWebhook(
      req.body.toString(),
      req.headers['slootly-signature'] as string,
      'whsec_your_webhook_secret'
    );

    switch (event.type) {
      case 'subscription.created':
        console.log('New subscriber:', event.data.subscriberId);
        console.log('Plan:', event.data.plan?.slug);
        break;
      case 'subscription.renewed':
        console.log('Renewed:', event.data.subscriberId);
        break;
      case 'subscription.canceled':
        console.log('Canceled:', event.data.subscriberId);
        break;
      case 'payment.failed':
        console.log('Payment failed:', event.data.subscriberId);
        break;
    }

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

Event Types

Event TypeDescriptionWhen It Fires
subscription.createdNew subscription createdAfter successful first payment
subscription.renewedSubscription renewedAfter successful recurring payment
subscription.updatedSubscription changedPlan change, metadata update
subscription.canceledSubscription canceledUser or API cancels subscription
payment.failedPayment failedCard declined, insufficient funds
test.pingTest eventWhen you click 'Send test event' in dashboard

Payload Schema

json
{
  "id": "evt_abc123def456",
  "type": "subscription.created",
  "created_at": "2026-03-19T12:00:00Z",
  "project_id": "proj_xyz789",
  "data": {
    "subscriber_id": "sub_abc123",
    "subscription_id": "subs_def456",
    "plan": {
      "id": "plan_abc123",
      "name": "Pro",
      "slug": "pro"
    },
    "status": "active",
    "platform": "telegram",
    "platform_user_id": "123456789",
    "current_period_start": "2026-03-19T00:00:00Z",
    "current_period_end": "2026-04-19T00:00:00Z",
    "cancel_at_period_end": false,
    "metadata": {}
  }
}

Signature Verification

Every webhook includes a Slootly-Signature header containing a timestamp and HMAC-SHA256 signature. The SDK handles verification automatically, but here is the manual process:

typescript
// Slootly-Signature header format:
// t=1679000000,v1=hmac_sha256_hex_signature

// Verification steps:
// 1. Extract timestamp (t) and signature (v1) from header
// 2. Construct signed payload: `${timestamp}.${rawBody}`
// 3. Compute HMAC-SHA256 with your webhook secret
// 4. Compare computed signature with v1 value
// 5. Verify timestamp is within 5 minutes of current time

Warning

Always verify webhook signatures before processing events. Unverified webhooks could be spoofed by attackers. The SDK methods verifyWebhook() and verify_webhook() handle this automatically.

Retry Behavior

If your endpoint returns a non-2xx status code or times out, Slootly retries the event with exponential backoff:

AttemptDelayTotal Time Elapsed
1st retry30 seconds30 seconds
2nd retry2 minutes2.5 minutes
3rd retry10 minutes12.5 minutes
4th retry1 hour1 hour 12.5 min
5th retry (final)4 hours5 hours 12.5 min

After 5 failed attempts, the event is marked as failed. You can manually retry failed events from the dashboard.

Best Practices

  • Return 200 quickly. Process events asynchronously. Return a 200 response immediately and handle business logic in a background job.
  • Handle duplicates. Use the event id field for idempotency. The same event may be delivered more than once during retries.
  • Verify signatures. Always use the SDK verification methods. Never skip signature verification.
  • Use raw body. Signature verification requires the raw request body, not a parsed JSON object. Configure your framework accordingly.