Gareth Wilson Gareth Wilson

Guide to Pine Labs Webhooks: Features and Best Practices

Published


Pine Labs Online, also branded Plural by Pine Labs, is an Indian payment gateway. Merchants use it to take payments on orders, issue refunds, save cards as network tokens, run subscriptions, pay out to bank accounts and settle cross-border import payments. Its webhooks report the outcome of each of these, so your systems learn when an order is paid, a refund fails, a token is suspended or a subscription halts after its retries run out.

What are Pine Labs webhooks?

Pine Labs sends every webhook as a JSON POST to a single URL that its support team registers for you during onboarding. There's no self-serve subscription page or per-event filter in the docs, so you receive every event type for your account.

Each delivery carries webhook-id, webhook-timestamp and webhook-signature headers in the Standard Webhooks layout, and Pine Labs makes verifying them mandatory. The key is the difference: Pine Labs signs with your dashboard secret key as plain bytes, not a base64 whsec_ secret. See verification.md in the skill.

Pine Labs also points out that webhooks are asynchronous. For time-sensitive decisions, it recommends polling the Get Order API instead.

Pine Labs webhook features

FeatureDetails
ConfigurationThrough Pine Labs support at onboarding
FilteringNone documented. Every event is sent
SignatureStandard Webhooks HMAC-SHA256 layout
Signature headerwebhook-signature
SecretDashboard secret key, used as raw bytes. No whsec_
BodyJSON
Event routingBody field event_type (eventType on payouts)
Response window2XX within 5 seconds
RetriesUp to 8 attempts over about 27.5 hours, per the retry-policy page
Source IPsPublished, separate lists for UAT and production

Events and payloads

Pine Labs documents 30 events:

EventTriggered WhenCommon Use Cases
CUSTOMER_ACTIVATEDThe customer is activated on Pine Labs OnlineMark the customer active
CUSTOMER_DELETEDThe customer is removed from Pine Labs OnlineClean up customer records
CUSTOMER_SUSPENDEDThe customer is temporarily suspended through the portalBlock new charges
CUSTOMER_CREATION_FAILEDCustomer creation failsRetry or alert
ORDER_AUTHORIZEDThe order is ready for authorizationPre-auth flows
ORDER_PROCESSEDThe payment is successfully receivedFulfil the order
ORDER_CANCELLEDThe order is cancelledRelease inventory
PAYMENT_FAILEDA payment against an order failsPrompt the customer to retry
ORDER_FAILEDThe order failsMark the order failed
REFUND_PROCESSEDA refund succeedsUpdate the order, notify the customer
REFUND_FAILEDA refund against an order failsAlert support
TOKEN_ACTIVATEDA card token is activated via the networkEnable saved-card payments
TOKEN_DEACTIVATEDThe token is deactivatedRemove the saved card
TOKEN_SUSPENDEDThe token is suspendedDisable the saved card
TOKEN_PROVISION_FAILEDToken provisioning failsFall back to a full card entry
SUBSCRIPTION_ACTIVATEDThe subscription is activatedGrant access
SUBSCRIPTION_PENDINGThe subscription is pendingShow a pending state
SUBSCRIPTION_PAUSEDThe subscription is pausedPause access
SUBSCRIPTION_RESUMEDThe subscription is resumedRestore access
SUBSCRIPTION_COMPLETEDThe subscription is completedClose it out
SUBSCRIPTION_CHARGEDA successful charge is madeRecord the payment
SUBSCRIPTION_HALTEDAll retries are exhausted and it moves from Pending to HaltedDunning, suspend access
SUBSCRIPTION_CANCELLEDThe subscription is cancelledRevoke access
SUBSCRIPTION_REVOKE_FAILEDAn attempt to revoke the subscription failsAlert support
SUBSCRIPTION_UPDATEDThe subscription is updatedSync plan details
SUBSCRIPTION_UPDATE_FAILEDAn attempt to update the subscription failsAlert support
payout-transaction-successThe payout is processedMark the payout paid
payout-transaction-failedThe payout failsRetry or alert
ORDER_SETTLEDCross-border import payments: the order is settledReconcile
ORDER_SETTLEMENT_REJECTEDCross-border import payments: settlement is rejectedInvestigate

Pine Labs' own description of ORDER_SETTLEMENT_REJECTED repeats ORDER_SETTLED's word for word, so its exact trigger is undocumented. Treat it as a settlement problem to investigate.

What data holds depends on the group:

Groupdata holds
Orders, payments, refunds, settlementorder_id, merchant_order_reference, type (CHARGE / REFUND), status, order_amount { value, currency }, purchase_details, and parent_order_id on refunds
Customerscustomer { customer_id, status, ... }
Tokenstoken { token_id, customer_id, status, payment_method, ... }
Subscriptionssubscription { subscription_id, status, plan_details, ... }. These payloads also carry top-level event_id and merchant_id
PayoutsSee below

Payout events use a different envelope. The name is camelCase eventType, with eventId, eventSource: "PAYOUT", eventVersion, sentAt and eventTimeStamp beside data, and the data fields are camelCase too: clientReferenceId, paymentReferenceId, status, bankTransactionReferenceId and amount. The Pay by Points order examples also use eventType, so read event_type ?? eventType before routing.

This is the CUSTOMER_ACTIVATED sample from the webhook samples library:

{
  "event_type": "CUSTOMER_ACTIVATED",
  "data": {
    "customer": {
      "customer_id": "cust-v1-0811030624-aa-RBDgpR",
      "merchant_customer_reference": "12345ABC",
      "first_name": "Kevin",
      "last_name": "Bob",
      "country_code": "91",
      "mobile_number": "9876543210",
      "email_id": "kevin.bob@example.com",
      "billing_address": {
        "address1": "10 Downing Street Westminster London",
        "address2": "Oxford Street Westminster London",
        "address3": "Baker Street Westminster London",
        "pincode": "51524036",
        "city": "Westminster",
        "state": "Westminster",
        "country": "London"
      },
      "shipping_address": {
        "address1": "10 Downing Street Westminster London",
        "address2": "Oxford Street Westminster London",
        "address3": "Baker Street Westminster London",
        "pincode": "51524036",
        "city": "Westminster",
        "state": "Westminster",
        "country": "London"
      },
      "gstin": "27ABCDE1234F2Z5",
      "merchant_metadata": {
        "key1": "XX",
        "key2": "DOF"
      },
      "status": "ACTIVE",
      "created_at": "2024-10-04T13:11:29.645591Z",
      "updated_at": "2024-10-04T13:11:29.645657Z"
    }
  }
}

Setting up Pine Labs webhooks

You need a Pine Labs Online merchant account, in UAT (sandbox) or production.

  1. Sign in to the Pine Labs Online dashboard, open Settings and copy your secret key into PINE_LABS_SECRET_KEY, exactly as shown.
  2. Contact Pine Labs support with your HTTPS endpoint, for example https://example.com/webhooks/pine-labs, and ask them to register it.
  3. If you restrict inbound traffic, allowlist Pine Labs' NAT gateway addresses from the webhooks overview page. UAT uses 15.206.155.74, 13.202.206.188, 13.202.213.132 and 13.126.41.142; production uses 18.61.136.235, 40.192.21.254, 18.60.11.185, 13.200.147.61 and 13.201.195.80/28.

To route Pine Labs through Hookdeck, create a source with the Pine Labs type and give support the source URL to register. When you add the secret, follow the source's configuration note:

Paste the secret key exactly as shown in the Pine Labs dashboard, without adding a whsec_ prefix.

During development, the Hookdeck CLI forwards the source to your machine:

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

Securing Pine Labs webhooks

Pine Labs' signature page walks through an example: a merchant key abc1234 is base64-encoded to YWJjMTIzNA== and then decoded again before use, which means the HMAC key is simply the raw bytes of the secret. Sign <webhook-id>.<webhook-timestamp>.<raw body> with HMAC-SHA256, base64-encode the result, and compare it with each v1, entry in webhook-signature.

This function uses a secret without the whsec_ prefix as raw bytes, which is what Pine Labs needs, and would base64-decode a whsec_ secret if it ever saw one:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['webhook-id'] !== undefined ? 'webhook' : 'svix';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = secret.startsWith('whsec_')
    ? decodeBase64(secret.slice(6)) : Buffer.from(secret, 'utf8');
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Don't use the standardwebhooks or svix libraries for Pine Labs. They always base64-decode the secret, derive a different key from Pine Labs' plain string, and reject every delivery.

The function applies no timestamp window. Hookdeck doesn't either, and it keeps the original webhook-timestamp on retries and replays, so a window would reject genuine events that Hookdeck delivers late.

Five seconds is a short window for payment processing, so the receiver only stores the event:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

// paymentEvents.record(webhookId, name, event) stores the event and resolves once it is durable.
function createPineLabsReceiver(paymentEvents) {
  const app = express();
  app.post('/webhooks/pine-labs', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.PINE_LABS_SECRET_KEY;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    const event = parseJsonObject(req.body);
    if (!event) return res.sendStatus(400); // signed, but not a JSON object
    const name = event.event_type ?? event.eventType; // payouts use camelCase
    try {
      await paymentEvents.record(req.headers['webhook-id'], name, event);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200); // unhandled event types are acknowledged too
  });
  return app;
}

If Pine Labs posts to a Hookdeck source and the signature doesn't match, Hookdeck still replies 200, records the request as failed verification and doesn't deliver it.

Make Pine Labs webhooks production-ready. Hookdeck Event Gateway answers Pine Labs well within five seconds and routes orders, subscriptions and payouts to the services that handle them.

Pine Labs webhook limitations and pain points

A five-second response window

The Problem: Deliveries are marked failed even though your handler eventually succeeds.

Why It Happens: Pine Labs counts a delivery as successful only if it gets a 2XX within five seconds.

Workarounds:

  • Store the event and respond before doing any processing.

How Hookdeck Can Help: Hookdeck answers Pine Labs immediately, and your service gets its own, longer timeout.

Conflicting retry documentation

The Problem: You can't size your recovery plan because you don't know how often Pine Labs retries.

Why It Happens: The retry-policy page lists eight attempts: immediately, then 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours and twice more at 10 hours. The overview page says "up to three times".

Workarounds:

  • Plan for the shorter policy, and reconcile with the Get Order API after any outage.
  • Ask Pine Labs support which policy applies to your account.

How Hookdeck Can Help: Hookdeck receives each event on the first attempt and retries your service on a schedule you control, whichever policy Pine Labs follows.

Events arrive out of order

The Problem: ORDER_PROCESSED arrives before ORDER_AUTHORIZED, and the order ends up in the wrong state.

Why It Happens: Pine Labs doesn't guarantee ordering, and gives that exact example.

Workarounds:

  • Never move an order back from a final state, and confirm with the Get Order API when in doubt.

How Hookdeck Can Help: Hookdeck's log shows the arrival order of every event for an order, which makes these cases easy to trace.

A non-standard secret

The Problem: Verification fails with an off-the-shelf Standard Webhooks library.

Why It Happens: Pine Labs uses the dashboard key as raw bytes, while the libraries expect a base64 whsec_ secret.

Workarounds:

  • Use a manual verifier like the one above, and store the key exactly as shown.

How Hookdeck Can Help: Hookdeck's Pine Labs source takes the key as shown and verifies it correctly, so your service doesn't need special handling.

Best practices

Deduplicate on webhook-id

It stays the same when Pine Labs resends an event. Record it in the same transaction as the order update; our webhook idempotency guide explains why.

Use merchant_order_reference to find orders

Order, payment and refund events carry your own reference beside Pine Labs' order_id.

Watch subscription failures

SUBSCRIPTION_HALTED means every retry has failed. Start dunning or suspend access, and alert on SUBSCRIPTION_REVOKE_FAILED and SUBSCRIPTION_UPDATE_FAILED.

Keep customer details out of logs

Customer events include names, phone numbers, email and address details. Log IDs only.

Conclusion

Pine Labs covers orders, tokens, subscriptions, payouts and cross-border settlement, with Standard Webhooks headers and published source IPs. Paste the secret key as shown, and expect payouts to use a camelCase envelope.

Hookdeck Event Gateway verifies Pine Labs' signature as documented, answers fast and keeps every payment event until your systems have handled it. Sign up for Hookdeck free.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.