Skip to main content

Webhook Event Handling

When billing activity occurs (such as a new purchase, monthly renewal, rescued payment, or chargeback), SolydFlow delivers a signed webhook to your backend server.

[1. Receive Signed Event] ➔ [2. Verify Signature] ➔ [3. Idempotency Check (event_id)] ➔ [4. Update Database & Return 200]

SolydFlow consolidates all billing lifecycles into four clean events, abstracting away provider-specific status codes and schemas.


The 4 Standard Billing Events​

Event NameWhen It FiresWhat Your Backend Should Do
subscription_startedInitial checkout succeeds, or user re-subscribes after expiry.Grant entitlement in your database until expires_at. Trigger onboarding flow.
subscription_renewedMonthly cycle renews, user upgrades, or Sweeper rescues stalled payment.Silently extend user's expires_at date in your database.
subscription_revokedChargeback filed, refund issued, or manual admin cancellation.Immediately revoke entitlement and lock premium features.
test_eventYou click "Send Test Webhook" in the Console API Vault.Validate your signature verification logic before going live.

Event Schemas​

1. subscription_started​

{
"event": "subscription_started",
"event_id": "evt_110e8400-e29b-41d4-a716-446655440001",
"environment": "live",
"is_test": false,
"user_id": "usr_supabase_991823",
"package_id": "pro_monthly_ngn",
"entitlement": "pro_access",
"provider": "paystack",
"expires_at": "2026-09-27T18:33:00Z"
}

2. subscription_renewed​

{
"event": "subscription_renewed",
"event_id": "evt_550e8400-e29b-41d4-a716-446655440000",
"environment": "live",
"is_test": false,
"user_id": "usr_supabase_991823",
"package_id": "pro_monthly_ngn",
"entitlement": "pro_access",
"provider": "paystack",
"expires_at": "2026-10-27T18:33:00Z"
}

3. subscription_revoked​

{
"event": "subscription_revoked",
"event_id": "evt_991f8400-e29b-41d4-a716-446655440001",
"environment": "live",
"is_test": false,
"user_id": "usr_supabase_991823",
"package_id": "pro_monthly_ngn",
"entitlement": "pro_access",
"reason": "chargeback",
"revoked_at": "2026-08-27T10:14:35Z"
}

Production Backend Implementation​

Your webhook handler must verify signatures, check for duplicate event_id deliveries, update your user records, and return HTTP 200 immediately:

src/webhookHandler.js
const express = require('express');
const app = express();

// Set to track processed event IDs (Use Redis or Postgres in production)
const processedEventIds = new Set();

app.post('/webhook/solydflow', express.raw({ type: 'application/json' }), async (req, res) => {
// 1. Verify HMAC Signature (see Webhooks > Signature Verification)
// ...

const payload = JSON.parse(req.body.toString());
const { event, event_id, user_id, entitlement, expires_at } = payload;

// 2. IDEMPOTENCY CHECK: Safely ignore duplicate deliveries
if (processedEventIds.has(event_id)) {
return res.status(200).json({ received: true, note: "duplicate_ignored" });
}
processedEventIds.add(event_id);

try {
// 3. Process Billing Lifecycle Events
switch (event) {
case "subscription_started":
case "subscription_renewed":
await db.users.update({
where: { id: user_id },
data: {
is_pro: true,
active_entitlement: entitlement,
access_expires_at: new Date(expires_at),
},
});
console.log(`✅ Granted ${entitlement} to user ${user_id} until ${expires_at}`);
break;

case "subscription_revoked":
await db.users.update({
where: { id: user_id },
data: {
is_pro: false,
active_entitlement: null,
},
});
console.log(`🔒 Revoked access for user ${user_id}. Reason: ${payload.reason}`);
break;

case "test_event":
console.log("🛠️ Test event received successfully");
break;

default:
console.warn("⚠️ Unhandled event type:", event);
}

// 4. Acknowledge with 200 OK immediately
return res.status(200).json({ received: true });
} catch (error) {
console.error("Database update failed:", error);
return res.status(500).send("Database error");
}
});

Best Practices​

  1. Acknowledge in <5 Seconds: Return HTTP 200 immediately. If you need to send welcome emails or generate invoices, dispatch them to a background queue (e.g. BullMQ, Celery, or AWS SQS).
  2. Handle Timestamps Safely: Always parse expires_at as ISO-8601 UTC.
  3. Ignore Test Events in Production: Inspect payload.is_test to prevent test webhook pings from overwriting real production customer records.