Agent skill

Pine Labs Webhooks Skill

Receive and verify Pine Labs Online (Plural by Pine Labs) webhooks. Use when setting up Pine Labs webhook handlers, debugging Standard Webhooks signature verification (webhook-id, webhook-timestamp, webhook-signature, secret key used as raw UTF-8 bytes), or handling events like ORDER_PROCESSED, PAYMENT_FAILED, REFUND_PROCESSED, SUBSCRIPTION_CHARGED, TOKEN_ACTIVATED and payout-transaction-success.

Install this skill

npx skills add hookdeck/webhook-skills --skill pine-labs-webhooks


Pine Labs Online (Plural by Pine Labs) is an Indian payment gateway. It sends webhooks for orders, payments, refunds, card tokens, subscriptions, customers, payouts and cross-border settlement.

When to Use This Skill

  • How do I receive Pine Labs webhooks?
  • How do I verify a Pine Labs webhook signature (webhook-signature)?
  • Why does Pine Labs signature verification fail with the standardwebhooks or svix library?
  • How do I handle ORDER_PROCESSED, PAYMENT_FAILED or REFUND_PROCESSED events?
  • How do I handle SUBSCRIPTION_CHARGED or payout-transaction-success?
  • Why am I processing the same Pine Labs event twice, or getting events out of order?

Verification (core)

Pine Labs uses the Standard Webhooks headers. Verification is mandatory per Pine Labs' docs.

HeaderValue
webhook-idEvent ID. Same when the webhook is resent
webhook-timestampUnix seconds when the event was triggered
webhook-signatureSpace-delimited list of v1,<base64> signatures

Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256, base64. The key is the secret key from the Pine Labs dashboard as raw UTF-8 bytes. There is no whsec_ prefix and nothing to base64-decode. Pine Labs' docs say to base64-encode the key and decode it again before use, which cancels out.

Hookdeck's configuration alert reads exactly: "Paste the secret key exactly as shown in the Pine Labs dashboard, without adding a whsec_ prefix." Put the secret in PINE_LABS_WEBHOOK_SECRET the same way.

import crypto from 'node:crypto';

// rawBody: the exact bytes received (express.raw() / request.text()), NOT re-serialised JSON.
// toleranceSec = 0 (default) skips the timestamp window, because Hookdeck retries and replays
// keep the original webhook-timestamp. A positive value opts in.
function verifyPineLabsWebhook(rawBody, { id, timestamp, signature }, secret, toleranceSec = 0) {
  if (!id || !signature || !/^[1-9][0-9]*$/.test(timestamp ?? '')) throw new Error('Missing or malformed headers');
  if (toleranceSec > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) {
    throw new Error('Timestamp outside tolerance');
  }
  const key = secret.startsWith('whsec_')
    ? Buffer.from(secret.slice(6), 'base64') // only if the secret itself starts with whsec_
    : Buffer.from(secret, 'utf8');           // Pine Labs: the secret as-is
  if (key.length === 0) throw new Error('Empty signing key');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest();
  const ok = signature.split(' ').some((entry) => {
    const [version, sig] = entry.split(',');
    const given = Buffer.from(sig ?? '', 'base64');
    return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
  if (!ok) throw new Error('Invalid signature');
  return JSON.parse(rawBody.toString('utf8'));
}

Header fallback: read the webhook-* headers and, when webhook-id is absent, use the svix-id, svix-timestamp and svix-signature headers instead, as Hookdeck does. The examples do this and test it.

Why not the standardwebhooks or svix libraries? They base64-decode the secret, so they compute the wrong key for Pine Labs' raw string secret. They also enforce a 5-minute timestamp window, which rejects Hookdeck retries and replays (they keep the original timestamp). The examples verify by hand and make the window opt-in with PINE_LABS_WEBHOOK_TOLERANCE_SECONDS (default 0).

The Python version is in references/verification.md.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Payload Shape, Retries and Ordering

Order, payment, refund, customer, token and subscription events are { "event_type": "...", "data": { ... } }. Payout events (and the Pay by Points order examples) use camelCase eventType with camelCase data fields, so read event_type ?? eventType.

{ "event_type": "ORDER_PROCESSED",
  "data": { "order_id": "v1-240909084141-aa-O2oJwd", "status": "PROCESSED",
            "order_amount": { "value": 200, "currency": "INR" } } }
  • Reply 2XX within 5 seconds. Anything else is retried: immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours and twice more at 10 hours. (The webhooks overview page says "up to three times", which conflicts with the retry policy page.)
  • Order is not guaranteed. ORDER_PROCESSED can arrive before ORDER_AUTHORIZED. Don't build a state machine on arrival order, and use the Get Order API when in doubt.
  • Deduplicate on webhook-id, which stays the same when an event is resent.

Common Event Types

Pine Labs documents 30 event types (the list may not be exhaustive). Names are case-sensitive, and the two payout events are lowercase with hyphens.

EventFired when
ORDER_AUTHORIZEDThe order is ready for authorization
ORDER_PROCESSEDThe payment is successfully received
ORDER_CANCELLED / ORDER_FAILEDThe order is cancelled / fails
PAYMENT_FAILEDA payment against an order fails
REFUND_PROCESSED / REFUND_FAILEDA refund succeeds / fails
SUBSCRIPTION_CHARGEDA successful charge is made on a subscription
SUBSCRIPTION_HALTEDAll retries are exhausted and the subscription moves to Halted
payout-transaction-success / payout-transaction-failedA payout is processed / fails

Also: CUSTOMER_ACTIVATED, CUSTOMER_DELETED, CUSTOMER_SUSPENDED, CUSTOMER_CREATION_FAILED, TOKEN_ACTIVATED, TOKEN_DEACTIVATED, TOKEN_SUSPENDED, TOKEN_PROVISION_FAILED, SUBSCRIPTION_ACTIVATED, SUBSCRIPTION_PENDING, SUBSCRIPTION_PAUSED, SUBSCRIPTION_RESUMED, SUBSCRIPTION_COMPLETED, SUBSCRIPTION_CANCELLED, SUBSCRIPTION_REVOKE_FAILED, SUBSCRIPTION_UPDATED, SUBSCRIPTION_UPDATE_FAILED, and for cross-border import payments ORDER_SETTLED and ORDER_SETTLEMENT_REJECTED. See references/overview.md.

Environment Variables

PINE_LABS_WEBHOOK_SECRET=your_pine_labs_secret_key   # Settings in the Pine Labs dashboard, pasted as-is (no whsec_ prefix)
PINE_LABS_WEBHOOK_TOLERANCE_SECONDS=0                # Optional replay window; a positive value opts in

Handlers fail closed: if PINE_LABS_WEBHOOK_SECRET is unset they answer 500 rather than accept unverified deliveries.

Local Development

npx hookdeck-cli listen 3000 pine-labs --path /webhooks/pine-labs

No account required. The CLI creates a guest account on first run and gives you a tunnel and a web UI for inspecting requests. Use port 8000 for the FastAPI example. Pine Labs configures the webhook URL for you during onboarding (see references/setup.md).

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →