# Guide to FastSpring Webhooks: Features and Best Practices

FastSpring webhooks notify your systems about commerce activity: an order completes, a subscription activates, a subscription is canceled. If you're building on FastSpring, webhooks are how you react to sales and subscription events without polling.

This guide covers how FastSpring webhooks work, the batched event format, how to verify the `X-FS-Signature`, deduplication, and the best practices for production.

## What are FastSpring webhooks?

FastSpring webhooks are HTTP POSTs delivered to endpoints you configure. Each POST is a JSON object with an `events` array that batches multiple events per request. FastSpring signs the whole delivery with an `X-FS-Signature` header: a base64 HMAC-SHA256 of the raw body, keyed with the per-webhook "HMAC SHA256 Secret". Signing is only active when you set that secret.

## FastSpring webhook features

| Feature | Details |
| --- | --- |
| Configuration | Developer Tools > Webhooks > Configuration (per-webhook HMAC secret) |
| Signature header | `X-FS-Signature` |
| Signature scheme | base64 HMAC-SHA256 of the raw body, keyed with the per-webhook secret |
| Signing | Only active when the secret is set |
| Payload | JSON with a batched `events` array |
| Deduplication | Each event has an `id`; auto-retries reuse it (manual retries get new ids) |
| Retries | Auto-retries until your endpoint returns HTTP 200 (HTTPS required) |
| Source IP | Optional: `107.23.30.83` |
| SDK | None |

## Common events

FastSpring events are dotted and cover orders and subscriptions:

| Event | Fires when |
| --- | --- |
| `order.completed` | An order completes |
| `subscription.activated` | A subscription activates |
| `subscription.canceled` | A subscription is canceled |
| `subscription.charge.completed` | A subscription charge completes |
| `return.created` | A return is created |

Each item in the `events` array has its own `type` and `id`. Consult FastSpring's webhook reference for the full list.

## Setting up FastSpring webhooks

Configure webhooks under Developer Tools > Webhooks > Configuration and set the "HMAC SHA256 Secret" (signing is only active once it's set). Endpoints must be HTTPS; optionally allowlist FastSpring's source IP. FastSpring auto-retries until your endpoint returns HTTP 200.

## Securing FastSpring webhooks

Verify the delivery once against the whole raw body, then iterate the `events` array. The `X-FS-Signature` header is a base64 HMAC-SHA256 of the raw request body, keyed with the per-webhook secret. Do not parse and re-serialize before verifying.

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

const SECRET = process.env.FASTSPRING_HMAC_SECRET; // per-webhook

function verify(rawBody, signature) {
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(rawBody)
    .digest("base64");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

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

  res.sendStatus(200); // acknowledge fast (retries continue until a 200)
  const { events } = JSON.parse(req.body);
  for (const event of events) processQueue.add(event); // dedupe on event.id, async
});

```

The same check in Python:

```python
import base64
import hashlib
import hmac
import os

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

def verify(raw_body: bytes, signature: str) -> bool:
    digest = hmac.new(SECRET, raw_body, hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    return hmac.compare_digest(expected, signature or "")

```

## FastSpring webhook limitations and pain points

### Batched events, one signature

The Problem: Each POST batches multiple events in `events[]`, but there's one signature over the whole body. Verifying per-event (or assuming one event per request) is wrong.

Why It Happens: FastSpring batches events for delivery efficiency and signs the whole payload.

Workarounds:

* Verify once over the raw body, then iterate `events` and process each item.

How Hookdeck Can Help: Hookdeck verifies the signature and can split the batch into individual events, so your app processes one clean event at a time.

### Signing is opt-in

The Problem: If you don't set the HMAC secret, deliveries are unsigned and any POST to the URL looks legitimate.

Why It Happens: FastSpring only signs when a secret is configured.

Workarounds:

* Set the secret and reject deliveries that don't verify; keep the URL secret and HTTPS-only.

How Hookdeck Can Help: Hookdeck gives you a dedicated ingestion URL and can enforce its own verification in front of your endpoint.

### Retries reuse the event id

The Problem: FastSpring auto-retries until it gets a 200, and automatic retries reuse the same event `id`, so the same event arrives repeatedly (manual retries get new ids).

Why It Happens: At-least-once delivery favors eventual delivery.

Workarounds:

* Dedupe on the event `id` for automatic retries, and make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so retries don't double-process. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Retries continue until a 200

The Problem: Any non-200 keeps FastSpring retrying, so a handler that returns 500 on a processing error triggers repeated deliveries.

Why It Happens: FastSpring treats non-200 as "not delivered".

Workarounds:

* Return 200 once the signature verifies and the batch is safely enqueued; handle processing errors internally.

How Hookdeck Can Help: Hookdeck returns success to FastSpring and retries to your downstream on its own schedule, so a transient error doesn't cause a retry storm.

## Best practices

### Verify once over the raw body, then iterate

Compute base64 HMAC-SHA256 over the raw body with the per-webhook secret, compare in constant time, then process each item in `events`.

### Set the secret

Configure the HMAC secret so deliveries are signed, and reject unsigned or invalid ones.

### Acknowledge with 200, process asynchronously

Return 200 once verified and enqueued, and handle processing failures internally. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Dedupe on event id

Dedupe on each event's `id` so automatic retries don't double-process.

## Conclusion

FastSpring webhooks batch multiple events in an `events[]` array under a single `X-FS-Signature` (base64 HMAC-SHA256 over the raw body, per-webhook secret). Verify once over the raw body, iterate the batch, set the secret so deliveries are signed, dedupe on event `id`, and return 200 to stop retries.

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

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