Files
english/.opencode/skills/payment-integration/references/paddle/webhooks.md
2026-04-12 01:06:31 +07:00

2.6 KiB

Paddle Webhooks

Event-driven notifications for payment lifecycle.

Setup

  1. Dashboard → Developer Tools → Notifications
  2. Create new destination with endpoint URL
  3. Select events to receive
  4. Copy signing secret

Signature Verification

Header: Paddle-Signature Format: ts=1234567890;h1=sha256_signature

Node.js SDK

import Paddle from '@paddle/paddle-node-sdk';

const paddle = new Paddle(process.env.PADDLE_API_KEY);

app.post('/webhooks/paddle', async (req, res) => {
  const signature = req.headers['paddle-signature'];
  const rawBody = req.body; // raw request body string

  try {
    const event = paddle.webhooks.unmarshal(
      rawBody,
      process.env.PADDLE_WEBHOOK_SECRET,
      signature
    );

    await handleEvent(event);
    res.status(200).send('OK');
  } catch (err) {
    res.status(400).send('Invalid signature');
  }
});

Manual Verification

import crypto from 'crypto';

function verifyPaddleWebhook(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const [tsPart, h1Part] = signature.split(';');
  const ts = tsPart.replace('ts=', '');
  const h1 = h1Part.replace('h1=', '');

  const signedPayload = `${ts}:${rawBody}`;
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(h1),
    Buffer.from(expectedSig)
  );
}

Key Events

Event Description
transaction.completed Payment successful
transaction.payment_failed Payment failed
subscription.created New subscription
subscription.updated Subscription changed
subscription.canceled Subscription canceled
subscription.past_due Payment overdue
subscription.paused Subscription paused
subscription.resumed Subscription resumed
customer.created New customer
customer.updated Customer updated

Event Payload

{
  "event_id": "evt_xxx",
  "event_type": "subscription.created",
  "occurred_at": "2024-01-15T10:00:00Z",
  "notification_id": "ntf_xxx",
  "data": {
    "id": "sub_xxx",
    "status": "active",
    "customer_id": "ctm_xxx",
    "items": [{ "price": { "id": "pri_xxx" }, "quantity": 1 }],
    "billing_cycle": { "interval": "month", "frequency": 1 },
    "current_billing_period": {
      "starts_at": "2024-01-15",
      "ends_at": "2024-02-15"
    }
  }
}

Best Practices

  • Store event_id for idempotency
  • Return 200 immediately, process async
  • Implement retry handling (Paddle retries failed deliveries)
  • Use webhook secret per environment