# Guide to Pylon Webhooks: Features and Best Practices

Pylon webhooks notify your systems about B2B support activity: an issue is created or updated, an account changes. If you're building on Pylon, webhooks are how you react to support events without polling.

This guide covers how Pylon webhooks work, the events you'll handle, how to verify the `Pylon-Webhook-Signature`, the retry behavior, and the best practices for production.

## What are Pylon webhooks?

Pylon webhooks are HTTP POSTs delivered to a webhook destination you configure, each signed with an HMAC-SHA256. The `Pylon-Webhook-Signature` header is `hs256=<hex>`, computed over `timestamp.payload`, with companion `Pylon-Webhook-Timestamp` and `Pylon-Webhook-Version` headers. The signing secret is shown only once when you create the destination.

## Pylon webhook features

| Feature | Details |
| --- | --- |
| Configuration | Webhook destinations (secret shown once at creation) |
| Signature header | `Pylon-Webhook-Signature`, formatted `hs256=<hex>` |
| Signature scheme | HMAC-SHA256 over `{timestamp}.{payload}`, keyed with the destination secret |
| Other headers | `Pylon-Webhook-Timestamp` (unix), `Pylon-Webhook-Version` |
| Retries | Up to 5 attempts with exponential backoff, final ~31h after the event |
| Inactivation | Destination flips to "inactive" after 7 days with no successful deliveries |
| SDK | None |

## Common events

Pylon events cover issues and accounts. The exact event-type token strings are gated behind Pylon's authenticated docs, so treat these as indicative and verify against a live Pylon account:

| Event | Fires when |
| --- | --- |
| `issue.created` | An issue is created |
| `issue.updated` | An issue is updated |

The precise format may be `issue.created` or `issue_created` (or trigger-based), so confirm the exact strings for your account before hardcoding them.

## Setting up Pylon webhooks

Create a webhook destination in Pylon and copy the signing secret, it's shown only once. Store it securely for verification.

## Securing Pylon webhooks

Each delivery carries a `Pylon-Webhook-Signature` header (`hs256=<hex>`) and a `Pylon-Webhook-Timestamp`. To verify, build `{timestamp}.{payload}` (the timestamp, a dot, then the raw body), compute an HMAC-SHA256 with the destination secret, hex-encode it, and compare against the hex after the `hs256=` prefix. Verify against the raw body, don't re-serialize.

```javascript
const crypto = require("crypto");

const SECRET = process.env.PYLON_WEBHOOK_SECRET;

function verify(rawBody, timestamp, signatureHeader) {
  const message = `${timestamp}.${rawBody.toString()}`;
  const expected = "hs256=" +
    crypto.createHmac("sha256", SECRET).update(message).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.headers["pylon-webhook-timestamp"];
  if (!verify(req.body, timestamp, req.headers["pylon-webhook-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

SECRET = os.environ["PYLON_WEBHOOK_SECRET"].encode()

def verify(raw_body: bytes, timestamp: str, signature_header: str) -> bool:
    message = (timestamp + "." + raw_body.decode()).encode()
    expected = "hs256=" + hmac.new(SECRET, message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

```

### The older X-Pylon-Signature scheme

An older Pylon support article documents a different header, `X-Pylon-Signature` (a hex HMAC-SHA256 of the raw body, with no timestamp). The `Pylon-Webhook-Signature` scheme above (from Pylon's developer guide) is the current one; if you encounter `X-Pylon-Signature`, it's the legacy variant.

## Pylon webhook limitations and pain points

### The signed message is `timestamp.payload`

The Problem: The HMAC is over `{timestamp}.{payload}`, not the body alone, and the header is `hs256=`-prefixed. Signing the body by itself, or dropping the prefix, fails.

Why It Happens: Pylon binds the timestamp into the signed message.

Workarounds:

* Build `timestamp + "." + body`, HMAC-SHA256, hex, and compare against the `hs256=` value over the raw body.

How Hookdeck Can Help: Hookdeck verifies the `Pylon-Webhook-Signature` at the edge, so your app receives pre-verified events without reconstructing the signed message.

### Two header schemes

The Problem: The current `Pylon-Webhook-Signature` and the legacy `X-Pylon-Signature` differ (the legacy one has no timestamp). Verifying with the wrong one fails.

Why It Happens: Pylon's documentation covers two generations.

Workarounds:

* Use the `Pylon-Webhook-Signature` scheme (the one the developer guide and this source use).

How Hookdeck Can Help: Hookdeck verifies the current scheme at the edge, so your app isn't coupled to which header is present.

### Secret shown once, and inactivation

The Problem: The signing secret appears only at creation, and the destination flips to "inactive" after 7 days with no successful deliveries. Lose the secret or let deliveries fail, and you're re-creating or reactivating.

Why It Happens: Pylon shows the secret once and prunes dead destinations.

Workarounds:

* Capture the secret at creation, acknowledge fast so deliveries succeed, and monitor destination status.

How Hookdeck Can Help: Hookdeck always accepts deliveries and absorbs downstream failures, keeping the destination active while it retries to your service.

### Unconfirmed event-type strings

The Problem: The exact event-type tokens are auth-gated, so a handler keyed to a guessed string may not match.

Why It Happens: Pylon's event catalog isn't publicly enumerable.

Workarounds:

* Confirm the exact event strings against a live account and handle unknown ones defensively.

How Hookdeck Can Help: Hookdeck's filters route on the event fields you actually receive, so you can adapt without redeploying handlers.

## Best practices

### Verify `hs256=` over `timestamp.body`

Build `{timestamp}.{body}`, HMAC-SHA256 with the destination secret, hex, and compare against the `hs256=` value in constant time, over the raw body.

### Store the secret and monitor status

Capture the once-shown secret, and watch for the 7-day inactivation.

### Acknowledge fast, process asynchronously

Return 200 quickly and defer work to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Confirm event names and dedupe

Verify the exact event strings against a live account, and dedupe so retries don't double-process. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Pylon webhooks verify with a `Pylon-Webhook-Signature` (`hs256=<hex>`) HMAC over `{timestamp}.{payload}`, keyed with a once-shown destination secret, with an older `X-Pylon-Signature` legacy variant to be aware of. Verify against the raw body, store the secret, confirm the exact event strings, and acknowledge fast to avoid the 7-day inactivation.

[Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Pylon webhooks reliably in minutes.