---
title: Webhooks
description: Insy posts signed JSON to your endpoint when memberships, payments and digital product purchases change. Delivery is at-least-once and unordered.
---

Instead of polling, you register an endpoint and Insy sends it a signed HTTP `POST` with a JSON body whenever a membership, payment or digital product purchase changes. There are exactly five event types — `membership.created`, `membership.updated`, `payment.success`, `payment.failed` and `digital.product.purchase` — documented on [Events](/developers/webhooks/events).

## Registering an endpoint

Register endpoints inside the Insy app, under the account that will receive the events. Management is first-party: there is no public REST API to create, list or delete webhooks, and no way to script registration.

1. **Add the endpoint in your Insy account**

    Provide the HTTPS URL that will receive deliveries.

2. **Copy the signing secret**

    32 random bytes as 64 hexadecimal characters, shown once when the webhook is created. Store it where your handler can read it (an env var). The list won't show it again, but saving the webhook returns it, so it is recoverable without recreating the endpoint.

3. **Verify and acknowledge**

    Your handler recomputes the signature over the raw body, compares it in constant time, then answers `2xx`. See [Verifying signatures](/developers/webhooks/verify).

## Who receives which events

A webhook belongs to a **user**, not to a community or product. Which events reach that user depends on their role:

| Events | Delivered to the webhooks of |
| --- | --- |
| `membership.created`, `membership.updated`, `payment.success`, `payment.failed` | The community's `owner`, `admin` and `moderator` members |
| `digital.product.purchase` | The creator of the product |

Every active webhook belonging to an eligible user receives the event.

> **There is no per-event subscription**
>
> You cannot subscribe a webhook to a subset of event types. Every active webhook of an eligible user receives all five types, so your handler must branch on `event_type` and ignore what it does not care about. Treat an unrecognised `event_type` as a no-op and still return `2xx` — new fields and future payload additions should not break you.

## The delivery contract

Each delivery is a `POST` to your URL with a JSON body and these headers:

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `X-Insy-Signature` | Lowercase hex HMAC-SHA256 of the raw request body |
| `X-Insy-Event-Id` | Event identifier, in the form `insy_event_<epochMillis>_<random>` |
| `X-Insy-Event-Type` | One of the five event types |

### Timing and retries

- A dispatcher runs **once a minute** and picks up to **100 pending deliveries** per run.
- Each request has a **10 second timeout**.
- **Any `2xx` counts as success.** The response body is ignored.
- Anything else — a non-`2xx` status, a connection error, a timeout — is a failure and is retried on a later dispatcher run, up to **3 attempts in total**. After the third attempt the delivery is abandoned.
- There is **no exponential backoff** and **no manual replay**. A delivery that exhausts its attempts is gone; you cannot ask Insy to send it again.

A burst can queue up behind the per-minute batch, so events arrive close to real time, not instantly — there is no latency guarantee.

> **Delivery is at-least-once and unordered**
>
> The same event can arrive more than once, and events can arrive out of order — `membership.updated` may land before the `membership.created` for the same membership, and a retried delivery can overtake a newer one. Make your handler idempotent: key on `X-Insy-Event-Id`, record ids you have already processed, and drop repeats. When you apply state, prefer the values in the payload over assumptions about what came before.

## Handling deliveries

- **Acknowledge, then process.** The 10 second timeout counts against you, so respond `2xx` immediately and do slow work from your own queue, where you control retries. A non-`2xx` buys two more attempts and nothing else.
- **Verify before you parse.** Your endpoint is public; anyone can post JSON to it. Run the HMAC check over the raw bytes first — see [Verifying signatures](/developers/webhooks/verify).
- **Reject forgeries with `401`.** An invalid signature is the one case where failing is right: a genuinely forged request keeps failing and is discarded, while a transient misconfiguration gets a couple more chances.
- **Log the raw body on verification failure**, at least while integrating — it is the only way to tell a wrong secret from a re-serialised body.

<CardGroup cols={2}>
  <Card title="Events" href="/developers/webhooks/events" icon="list">
    The shared envelope and the payload of all five event types.
  </Card>
  <Card title="Verifying signatures" href="/developers/webhooks/verify" icon="shield-check">
    The HMAC scheme, the hex-decoding trap, and runnable examples.
  </Card>
</CardGroup>
