T
Trickl

Webhooks Documentation

Receive real-time notifications when payment events occur in your Trickl subscriptions.

1Quick Start

  1. 1Set up an endpoint on your server to receive POST requests
  2. 2Configure your webhook URL in the Trickl Dashboard under Settings
  3. 3Save your webhook secret securely - you'll use it to verify signatures
  4. 4Test your endpoint using the built-in test feature

2Webhook Events

EventDescription
payment.completedA round-up payment was successfully processed
payment.failedA payment attempt failed
subscription.createdA new user subscribed to your service
subscription.cancelledA user cancelled their subscription
subscription.pausedA subscription was paused
Goal & Platform API Events
goal.completedA one-time savings goal was fully funded and paid out
goal.deposit_paidA buyer paid the upfront deposit on a goal
goal.subscription_createdA buyer confirmed a recurring subscription via micro-payments
goal.cycle_paidA billing cycle was completed and paid out for a recurring subscription
goal.cancelledA savings goal or subscription was cancelled

3Payload Structure

All webhook payloads follow this structure:

{
  "id": "whevt_a1b2c3d4e5f6...",
  "type": "payment.completed",
  "timestamp": 1706745600,
  "data": {
    "subscriptionId": "sub_abc123",
    "userId": "user_xyz789",
    "userEmail": "customer@example.com",
    "userName": "John Doe",
    "providerId": "prov_123abc",
    "tierName": "Premium",
    "amount": 2.50,
    "totalPaidThisCycle": 7.50,
    "subscriptionAmount": 9.99,
    "paymentProgress": 75,
    "nextPaymentDue": "2024-02-15T00:00:00.000Z",
    "paymentIntentId": "pi_...",
    "transferId": "tr_..."
  }
}

3bGoal & Subscription Payloads

Goal events use a different payload structure. These are sent when using the Platform API to create goals and subscriptions.

goal.subscription_created

{
  "id": "whevt_e5f6g7h8...",
  "type": "goal.subscription_created",
  "timestamp": 1709500800,
  "data": {
    "goalId": "goal_abc123",
    "userId": "trickl_user_id",
    "userEmail": "buyer@example.com",
    "userName": "Jane Doe",
    "providerId": "prov_abc123",
    "frequency": "MONTHLY",
    "nextBillingDate": "2026-04-03T00:00:00.000Z",
    "metadata": { "planId": "plan_premium" }
  }
}

// Fired when a buyer confirms a recurring subscription.
// Grant access to your content/service.

goal.cycle_paid

{
  "id": "whevt_i9j0k1l2...",
  "type": "goal.cycle_paid",
  "timestamp": 1709500800,
  "data": {
    "goalId": "goal_abc123",
    "userId": "trickl_user_id",
    "userEmail": "buyer@example.com",
    "userName": "Jane Doe",
    "providerId": "prov_abc123",
    "amount": 9.99,
    "transferId": "tr_...",
    "frequency": "MONTHLY",
    "cycleNumber": 2,
    "nextBillingDate": "2026-05-03T00:00:00.000Z",
    "metadata": { "planId": "plan_premium" }
  }
}

// Fired each billing cycle. Renew the buyer's access.
// savedAmount resets to 0 — round-ups resume automatically.

goal.completed

{
  "id": "whevt_a1b2c3d4...",
  "type": "goal.completed",
  "timestamp": 1709500800,
  "data": {
    "goalId": "goal_xyz789",
    "userId": "trickl_user_id",
    "userEmail": "buyer@example.com",
    "userName": "Jane Doe",
    "providerId": "prov_abc123",
    "amount": 20.00,
    "depositAmount": 9.99,
    "transferId": "tr_...",
    "metadata": { "orderId": "order_123" }
  }
}

// Fired when a one-time goal is fully funded.
// Grant product access to the buyer.

goal.cancelled

{
  "id": "whevt_m3n4o5p6...",
  "type": "goal.cancelled",
  "timestamp": 1709500800,
  "data": {
    "goalId": "goal_xyz789",
    "userId": "trickl_user_id",
    "userEmail": "buyer@example.com",
    "userName": "Jane Doe",
    "providerId": "prov_abc123",
    "amount": 29.99,
    "depositRefunded": true,
    "metadata": { "orderId": "order_123" }
  }
}

// Fired for both goal and subscription cancellations.
// Revoke access on your platform.

4Security

Security Headers

Every webhook request includes these headers for verification:

HeaderDescription
X-Trickl-SignatureHMAC-SHA256 signature of the payload
X-Trickl-TimestampUnix timestamp when webhook was sent
X-Trickl-Webhook-IdUnique ID for this webhook delivery

Important: Always Verify Signatures

Never process a webhook without verifying its signature. This protects you from spoofed requests.

5Code Examples

const crypto = require('crypto');
const express = require('express');
const app = express();

// Use raw body for signature verification
app.use('/webhooks/trickl', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.TRICKL_WEBHOOK_SECRET;

function verifySignature(payload, timestamp, signature) {
  const signaturePayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signaturePayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

function isTimestampValid(timestamp) {
  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) <= 300; // 5 minute tolerance
}

app.post('/webhooks/trickl', (req, res) => {
  const signature = req.headers['x-trickl-signature'];
  const timestamp = parseInt(req.headers['x-trickl-timestamp']);
  const payload = req.body.toString();

  // Verify timestamp (replay protection)
  if (!isTimestampValid(timestamp)) {
    return res.status(400).json({ error: 'Timestamp expired' });
  }

  // Verify signature
  if (!verifySignature(payload, timestamp, signature)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Parse and process the event
  const event = JSON.parse(payload);
  console.log(`Received ${event.type} webhook`);

  switch (event.type) {
    case 'payment.completed':
      // Handle payment completed
      break;
    case 'subscription.created':
      // Handle new subscription
      break;
    case 'subscription.cancelled':
      // Handle cancellation
      break;
    case 'goal.completed':
      // One-time goal funded — grant product access
      break;
    case 'goal.subscription_created':
      // Recurring subscription confirmed — grant access
      break;
    case 'goal.cycle_paid':
      // Billing cycle paid — renew access
      break;
    case 'goal.cancelled':
      // Goal or subscription cancelled — revoke access
      break;
  }

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

app.listen(3000);

6Best Practices

Respond Quickly

Return a 200 response immediately. Process webhooks asynchronously using a job queue.

Handle Idempotency

Use the webhook ID to prevent processing the same event twice. Store processed IDs in your database.

Handle Retries

Trickl retries failed webhooks 3 times with exponential backoff (1s, 5s, 15s).

Use HTTPS

In production, webhook URLs must use HTTPS to protect payload data.

7Testing Webhooks

Using the Dashboard

  1. 1. Go to Settings → Webhooks in your dashboard
  2. 2. Enter your endpoint URL
  3. 3. Click "Test Webhook"
  4. 4. Check your server logs for the test event

Local Development

Use a tunnel service like ngrok for local testing:

# Start ngrok
ngrok http 3000

# Use the ngrok URL in your Trickl dashboard
# https://abc123.ngrok.io/webhooks/trickl

Need Help?

Our support team is here to help you set up webhooks.

Contact Support