# Guide to Stigg Webhooks: Features and Best Practices

Stigg webhooks tell your systems when something changes in your pricing and entitlements setup: a customer signs up, a subscription is created, upgraded or canceled, a trial converts, a customer's entitlements change, usage crosses a limit, or a credit balance runs low. If you use Stigg to run plans and feature access, webhooks are how those changes reach your CRM, billing, analytics and provisioning code.

This guide covers how Stigg webhooks are set up and authenticated, the events and the shape of their payloads, the gaps in Stigg's documentation, and the best practices for running a Stigg receiver in production.

## What are Stigg webhooks?

A Stigg webhook is an endpoint URL plus a list of events, configured in the Stigg app. When one of those events happens, Stigg sends a JSON `POST` to the URL. Webhooks are active as soon as you create them.

Stigg doesn't sign its webhooks. Each webhook has a secret, and Stigg sends that secret unchanged in a `Stigg-Webhooks-Secret` header on every request. Your receiver authenticates a request by checking that the header matches the secret on the webhook's details page. Stigg says HMAC signature support is on its roadmap.

The body is flat. The event name is in `type`, and the changed entity's fields sit beside it at the top level, along with a `messageId` for de-duplication on most events.

## Stigg webhook features

| Feature | Details |
| --- | --- |
| Configuration | **Settings > Integrations > Webhooks** in the Stigg app |
| Fields | Service name, endpoint URL, events to send |
| Method and format | `POST`, `application/json` |
| Authentication | The webhook secret, sent verbatim in `Stigg-Webhooks-Secret` |
| Signature | None. HMAC signing is on Stigg's roadmap |
| Event name | `type` in the body |
| De-duplication key | `messageId` in the body |
| Response window | A `2xx` within 30 seconds |
| Retries | 3 immediate retries, then 3 retries 30 seconds apart |
| After retries | The event is marked failed, but Stigg stores it and can resend it on request |
| Ordering | Not guaranteed |
| Testing | **Send test event**, which shows the request body and your endpoint's response |
| Source IP | `18.119.35.43` (Stigg says the list may change and it will notify you) |

Stigg also offers durable queues (Amazon SQS) as an alternative way to receive the same kinds of events, if you'd rather not run an HTTP endpoint.

## Common events

Stigg publishes its webhook events as an OpenAPI document with a schema and example for each one. The main groups are:

| Area | Events |
| --- | --- |
| Customers | `customer.created`, `customer.updated`, `customer.deleted`, `customer.payment_failed` |
| Subscriptions | `subscription.created`, `subscription.updated`, `subscription.canceled`, `subscription.expired`, `subscription.create_failed`, `subscription.billing_month.ends_soon`, `subscription.spend_limit.exceeded` |
| Trials | `subscription.trial_started`, `subscription.trial.ends_soon` (7 days before expiry), `subscription.trial_converted`, `subscription.trial_expired` |
| Entitlements | `entitlements.updated`, `entitlement.usage_exceeded`, `promotionalEntitlement.granted`, `.updated`, `.revoked`, `.ends_soon`, `.expired` |
| Credits | `credit_grant.created`, `.updated`, `.balance.low`, `.depleted`, `.expired`, `credit_balance.low`, `credit_balance.depleted`, `automatic_recharge.operation.attempted`, `automatic_recharge.configuration.changed`, `credits.automatic_recharge_limit_exceeded` |
| Catalog | `product.*`, `plan.*`, `addon.*` and `feature.*` (`created`, `updated`, `deleted`), `feature_group.*` (`created`, `updated`, `archived`, `un_archived`), `coupon.*` (`created`, `updated`, `archived`) |
| Integrations | `sync.failed`, `data_export.sync_failed` |

The naming is not consistent, so copy names exactly: `subscription.trial_started` but `subscription.trial.ends_soon`, and camelCase `promotionalEntitlement.*` beside snake_case everywhere else.

Here is Stigg's `entitlements.updated` example, trimmed:

```json
{
  "messageId": "ce2605be-8eed-41cf-912c-def052682dbe",
  "traceId": "49146c12-8af0-44ff-a666-1f46255aecd8",
  "timestamp": "2025-11-13T21:41:56.923Z",
  "type": "entitlements.updated",
  "trigger": "subscription_updated",
  "customer": {
    "id": "f29e950f-99cb-4f06-8395-434fb5259a58",
    "name": "Ryan Bukoski's Organization",
    "email": "test@example.com",
    "billingId": "cus_TPyDVBZKeaQgnx"
  },
  "resource": {
    "id": "device-34ea9346-4a37-4c5d-82b1-0e42c45dcd77",
    "entityId": "14edaa7c-5326-41a1-99bb-b6365f0a0822"
  },
  "entitlements": [
    {
      "feature": {
        "id": "feature-1-tb-cloud-storage",
        "name": "1 TB cloud storage",
        "featureType": "NUMBER",
        "meterType": "INCREMENTAL",
        "unit": "Tbs"
      }
    }
  ]
}

```

Other events carry the entity itself at the top level. `subscription.trial_converted`, for example, has the subscription's `id`, `entityId`, `status`, billing period dates, `isTrialSubscription` and a `reason` such as `TRIAL_ENDED` next to `type`. Entities carry both an `id` and an `entityId`. In Stigg's examples, `id` is a readable identifier and `entityId` is a UUID, so pick one and use it consistently as your key.

## Setting up Stigg webhooks

1. In the Stigg app, go to **Settings > Integrations > Webhooks** and click **+ New webhook**.
2. Enter a service name, your endpoint URL, and the events to send, then click **Add**. The webhook starts sending immediately.
3. Open the webhook's details page, copy its secret, and store it server-side, for example as `STIGG_WEBHOOK_SECRET`.
4. Use **Send test event** on one of the webhook's events to check that your endpoint answers with a `2xx`.

If you use Hookdeck, create a Stigg source, paste the webhook secret into it, and use the Hookdeck source URL as the endpoint URL in Stigg. Hookdeck compares the `Stigg-Webhooks-Secret` header with that value. It accepts every request with a `200` first and checks the header afterwards, so a request with a missing or wrong secret is recorded as failed verification and is not delivered to your service, but Stigg still sees a success.

## Securing Stigg webhooks

There is nothing to compute. Read the `Stigg-Webhooks-Secret` header and compare it with your stored secret in constant time. Because the body isn't signed, you can parse it however you like.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyStiggSecret(headerValue, expectedSecret) {
  if (!headerValue || !expectedSecret) return false;
  const received = Buffer.from(headerValue, 'utf8');
  const expected = Buffer.from(expectedSecret, 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/stigg', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.STIGG_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyStiggSecret(req.get('stigg-webhooks-secret'), secret)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  // Route on event.type, deduplicate on event.messageId, then process asynchronously.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac

def verify_stigg_secret(header_value, expected_secret: str) -> bool:
    if not header_value or not expected_secret:
        return False
    return hmac.compare_digest(header_value.encode("utf-8"), expected_secret.encode("utf-8"))

# In FastAPI: verify_stigg_secret(request.headers.get("stigg-webhooks-secret"),
#                                 os.environ["STIGG_WEBHOOK_SECRET"])

```

The secret travels with every request, so anyone who captures one request can send forged ones. Serve the endpoint over HTTPS only. Stigg publishes a source IP, `18.119.35.43`, which you can allowlist as a second check, keeping in mind that Stigg says the list may change.

> **Make Stigg webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the `Stigg-Webhooks-Secret` header, accepts every subscription and entitlement event as soon as it arrives, and durably queues it, so a slow deploy doesn't use up Stigg's short retry schedule.

## Stigg webhook limitations and pain points

### A shared secret instead of a signature

**The Problem:** Nothing proves that the body you received is the body Stigg sent.

**Why It Happens:** Stigg sends the secret itself in a header and signs nothing. A captured request can be replayed or altered and its header still matches.

**Workarounds:**

* Use HTTPS, and allowlist Stigg's published IP address.
* For actions that grant or remove access, treat the webhook as a notification and read the current state from the Stigg API before acting.
* Watch Stigg's changelog for HMAC signing, which it lists as on its roadmap.

**How Hookdeck Can Help:** Hookdeck checks the header for you, so the secret lives on the Hookdeck source and not in every service, and requests with the wrong secret are recorded as failed verification instead of being delivered.

### A short retry schedule

**The Problem:** An outage of a couple of minutes is enough to lose events.

**Why It Happens:** Stigg retries an error or timeout 3 times without backoff, then 3 times 30 seconds apart. After that the event is marked failed. Stigg stores failed events and can resend them on request, but there is no self-service log yet, and events that happen while a webhook is disabled are never sent at all.

**Workarounds:**

* Keep the receiver's job small: check the secret, store the event, return `200`.
* Alert on consecutive failures, as Stigg recommends, and contact Stigg to resend events after an incident.
* Don't disable a webhook to pause traffic. Missed events can't be replayed when it is re-activated.

**How Hookdeck Can Help:** Hookdeck accepts each delivery as soon as it arrives and stores it, then retries delivery to your service on a schedule you control and lets you replay anything that failed. Pausing a Hookdeck connection holds events rather than dropping them.

### Events arrive out of order

**The Problem:** A `subscription.updated` can be processed before the `subscription.created` it follows.

**Why It Happens:** Stigg doesn't guarantee delivery order, and retries make reordering more likely.

**Workarounds:**

* Compare timestamps on the entity, or fetch the latest state from the Stigg REST or GraphQL API, before you overwrite local data.
* Make each handler safe to run before or after its neighbours.

**How Hookdeck Can Help:** Hookdeck records when each event arrived and lets you replay events individually or in bulk when you need to rebuild state.

### `messageId` is not on every example

**The Problem:** Deduplication that relies on `messageId` can fail on some event types.

**Why It Happens:** Stigg's webhook page says every event has a `messageId`, but several of the examples in its OpenAPI, including `subscription.trial_converted` and the `product`, `credit_grant` and `automatic_recharge` events, show none.

**Workarounds:**

* Deduplicate on `messageId` when it is present.
* Fall back to a key built from `type`, the entity's `id` or `entityId`, and a timestamp field.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeated deliveries based on whichever body fields you choose.

### Two vocabularies for credit events

**The Problem:** Your handler matches credit events by names that never arrive.

**Why It Happens:** Stigg's Credits page names events such as `credits.granted`, `credits.balance.usage_low` and `credit_type.created`, while the webhook OpenAPI uses `credit_grant.*` and `credit_balance.*` and has no credit type events.

**Workarounds:**

* Subscribe to the credit events you need, send a test event, and log the `type` you receive.
* Route unknown types to a log rather than an error.

**How Hookdeck Can Help:** Hookdeck shows every request it receives, with its full body, so you can see exactly which `type` values Stigg sends before you write filters for them.

## Best practices

### Compare the secret in constant time

Use `crypto.timingSafeEqual` or `hmac.compare_digest` rather than `===` or `==`, and check lengths first in Node. Fail closed if the secret isn't configured.

### Route on `type` and keep unknown events harmless

Stigg adds events over time. Switch on `type`, handle the ones you need, and acknowledge the rest with `200`.

### Use `entitlements.updated` for access decisions

If your product gates features, `entitlements.updated` carries the customer's effective entitlements after a change, whatever caused it (`trigger`). Recomputing access from that event is simpler than inferring it from subscription, add-on and promotion events separately.

### Acknowledge fast and process asynchronously

Stigg allows 30 seconds, but a fast acknowledgement keeps you clear of the retry schedule. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Stigg authenticates webhooks with a shared secret in the `Stigg-Webhooks-Secret` header rather than a signature. Compare it in constant time over HTTPS, route on `type`, deduplicate on `messageId` with a fallback key, expect events out of order, and acknowledge quickly, because Stigg stops retrying after about a minute and a half.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the Stigg secret, and holds each event until your service has processed it.

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