Gareth Wilson Gareth Wilson

Guide to Stigg Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationSettings > Integrations > Webhooks in the Stigg app
FieldsService name, endpoint URL, events to send
Method and formatPOST, application/json
AuthenticationThe webhook secret, sent verbatim in Stigg-Webhooks-Secret
SignatureNone. HMAC signing is on Stigg's roadmap
Event nametype in the body
De-duplication keymessageId in the body
Response windowA 2xx within 30 seconds
Retries3 immediate retries, then 3 retries 30 seconds apart
After retriesThe event is marked failed, but Stigg stores it and can resend it on request
OrderingNot guaranteed
TestingSend test event, which shows the request body and your endpoint's response
Source IP18.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:

AreaEvents
Customerscustomer.created, customer.updated, customer.deleted, customer.payment_failed
Subscriptionssubscription.created, subscription.updated, subscription.canceled, subscription.expired, subscription.create_failed, subscription.billing_month.ends_soon, subscription.spend_limit.exceeded
Trialssubscription.trial_started, subscription.trial.ends_soon (7 days before expiry), subscription.trial_converted, subscription.trial_expired
Entitlementsentitlements.updated, entitlement.usage_exceeded, promotionalEntitlement.granted, .updated, .revoked, .ends_soon, .expired
Creditscredit_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
Catalogproduct.*, plan.*, addon.* and feature.* (created, updated, deleted), feature_group.* (created, updated, archived, un_archived), coupon.* (created, updated, archived)
Integrationssync.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:

{
  "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.

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:

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 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 and our guide to 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 sits in front of your receiver, checks the Stigg secret, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Stigg webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

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