> ## Documentation Index
> Fetch the complete documentation index at: https://orgo.space/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Push member, payment, registration, contract, role and contact events to your own systems in real time

A webhook tells your system that something happened in Orgo the moment it happens. Orgo sends an HTTP POST with the full record to a URL you control, so a CRM, a data warehouse or an automation platform stays current without polling the API.

**Built for** teams keeping another system in step with Orgo.
**Replaces** scheduled exports and "sync every 15 minutes" scripts.

<img src="https://mintcdn.com/orgo-dc7abe63/BcaQGMmLlvrr9oyS/images/platform/developers/webhooks-list.png?fit=max&auto=format&n=BcaQGMmLlvrr9oyS&q=85&s=bef99f4c7f9e22060bc3e1207a36ea29" alt="Webhooks page listing subscriptions with their URL, subscribed events, status, success and failure counts, last trigger time and per-row test, logs, edit and delete actions" style={{ width: "100%", borderRadius: "8px", border: "1px solid var(--border-color)", marginBottom: "1rem" }} width="3840" height="2160" data-path="images/platform/developers/webhooks-list.png" />

***

## Where it lives

**Settings → Developers → Webhooks**

Creating, editing and deleting a subscription requires **ADMIN\_TENANT**. Testing a webhook and reading its delivery logs are available on the same page.

***

## Creating a subscription

<img src="https://mintcdn.com/orgo-dc7abe63/BcaQGMmLlvrr9oyS/images/platform/developers/webhook-create.png?fit=max&auto=format&n=BcaQGMmLlvrr9oyS&q=85&s=93b34844cfe151e0619dadc197d53c72" alt="Create Webhook form with the Active switch, name, endpoint URL, description and the six event type groups" style={{ width: "100%", borderRadius: "8px", border: "1px solid var(--border-color)", marginBottom: "1rem" }} width="3840" height="2160" data-path="images/platform/developers/webhook-create.png" />

<Steps>
  <Step title="Name the webhook">
    A descriptive name: the receiving system, not the event.
  </Step>

  <Step title="Enter the endpoint URL">
    Must be `https` and must resolve to a public address. A URL that resolves to a private or loopback address is rejected when you save it, and again at delivery time.
  </Step>

  <Step title="Pick the event types">
    At least one. They are grouped by family (see below).
  </Step>

  <Step title="Save, then test">
    The play button on the list sends a synthetic `webhook.test` delivery to the URL so you can confirm your endpoint answers before real traffic arrives.
  </Step>
</Steps>

***

## Events you can subscribe to

| Group                | Events in the UI                                                   |
| -------------------- | ------------------------------------------------------------------ |
| **User**             | User Created, User Updated, User Deleted                           |
| **Contact**          | Contact Created, Contact Updated, Contact Deleted                  |
| **Payment**          | Payment Created, Payment Updated, Payment Deleted                  |
| **Event Attendance** | Registration Created, Registration Updated, Registration Cancelled |
| **Contract**         | Contract Signed, Contract Updated, Contract Removed                |
| **Role**             | Role Assigned, Role Updated, Role Removed                          |

Eighteen events in total. Each delivery carries the full record as it stands at that moment, and update events also carry the fields that changed. Passwords, tokens and connected-account identifiers are stripped before sending.

The exact payload envelope, headers and per-event schemas are in the [Webhooks reference](/docs/api-reference/concepts/webhooks).

***

## Settings reference

| Field                     | What it does                                                                                                                                                                                 |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **active**                | Turns delivery on or off without deleting the subscription. An inactive subscription receives nothing.                                                                                       |
| **Webhook Name**          | Label shown in the list. Required.                                                                                                                                                           |
| **Endpoint URL**          | Where the POST goes. Required, `https`, publicly resolvable.                                                                                                                                 |
| **Description**           | Free text for your own reference.                                                                                                                                                            |
| **Event Types**           | Which events trigger this webhook. At least one required.                                                                                                                                    |
| **Max Retries**           | Retry attempts after a failed delivery. 0 to 10, default 3.                                                                                                                                  |
| **Timeout (seconds)**     | How long Orgo waits for your response. 1 to 300, default 30.                                                                                                                                 |
| **Webhook Secret**        | Optional. When set, every delivery carries an `X-Webhook-Signature` header: an HMAC-SHA256 of the delivery, keyed with this secret. See [Verifying the signature](#verifying-the-signature). |
| **Custom Headers (JSON)** | Extra HTTP headers sent with every delivery, for example an `Authorization` header your endpoint checks.                                                                                     |
| **Metadata (JSON)**       | Notes stored on the subscription. Never sent with the request.                                                                                                                               |

Secret, custom headers and metadata sit behind **Advanced Options** on the form.

***

## Verifying the signature

Set a **Webhook Secret** and every delivery to that subscription carries a signature your endpoint can check, so you can prove the request came from Orgo and not from someone who found your URL. Use a long random string, at least 32 characters, and store it in your own configuration exactly as you typed it here.

The header looks like this:

```
X-Webhook-Signature: t=1754413200,v1=<64 lowercase hex characters>
X-Webhook-Timestamp: 1754413200
```

`t` is the moment Orgo signed the delivery, in Unix seconds, and repeats the value in `X-Webhook-Timestamp`. `v1` is an HMAC-SHA256, hex encoded, over the string `t`, a full stop, and the request body exactly as it arrived:

```
v1 = HMAC-SHA256(key = your webhook secret, message = "<t>.<raw request body>")
```

<Steps>
  <Step title="Read the raw body before anything parses it">
    Sign the bytes you received. A body that has been decoded and re-encoded to JSON is a different string, and the signature will not match.
  </Step>

  <Step title="Split the header on the comma">
    Take the value after `t=` and the value after `v1=`. Reject the delivery if the header is missing and you expect one.
  </Step>

  <Step title="Recompute the HMAC">
    Concatenate `t`, `.` and the raw body, run HMAC-SHA256 over it with your secret as the key, and hex encode the result.
  </Step>

  <Step title="Compare in constant time">
    Use your language's constant-time comparison (`hash_equals`, `crypto.timingSafeEqual`, `hmac.compare_digest`), not `==`. Reject the delivery if it differs.
  </Step>

  <Step title="Reject deliveries that are too old">
    Compare `t` against your own clock and refuse anything outside a tolerance you choose, five minutes being the usual figure. Because `t` is inside the signed string, nobody can change it without breaking the signature, which is what makes a captured delivery unusable later.
  </Step>
</Steps>

Working receivers in Node and Python are in the [Webhooks reference](/docs/api-reference/concepts/webhooks#authenticating-the-sender).

Two things to know before you set a tolerance. Each retry is signed again at the moment it is sent, so a redelivery arrives with a fresh `t` and a different `v1`, and a short window does not cause retries to fail. And because the same delivery can therefore carry different signatures, deduplicate on the payload `id` or on `X-Webhook-Delivery`, never on the signature.

<Warning>
  The signature format changed in August 2026. Until then the header carried a short non-cryptographic checksum, which could be reproduced by anyone who had seen one delivery. A receiver written against the old value must be updated to the recipe above; the old header is not sent alongside the new one. Deliveries keep arriving either way, so nothing stops working while you migrate, but a receiver still comparing the old value is authenticating nothing.
</Warning>

***

## Delivery and retries

Deliveries are queued, not sent inside the request that caused them, so a slow endpoint never slows down the app.

* A `2xx` response marks the delivery successful.
* Anything else, a timeout or a TLS error counts as a failure and is retried until **Max Retries** is reached.
* Retries back off exponentially, starting around 20 seconds and capped at 10 minutes between attempts.
* Every attempt is logged, including the ones that eventually succeed.

A subscription is never deactivated automatically after repeated failures. Watch the Success / Failed / Total counters on the list, or your own monitoring.

Because retries exist, the same event can arrive more than once. Make your handler idempotent by keying on the delivery's `id`.

***

## Delivery logs

The list icon on each row opens the delivery log for that subscription.

<img src="https://mintcdn.com/orgo-dc7abe63/BcaQGMmLlvrr9oyS/images/platform/developers/webhook-logs.png?fit=max&auto=format&n=BcaQGMmLlvrr9oyS&q=85&s=22225ccbe8c8ab8d851ad114da13c394" alt="Webhook delivery log table showing status, event type, response code, response time, attempt number, delivery ID and timestamp for recent attempts" style={{ width: "100%", borderRadius: "8px", border: "1px solid var(--border-color)", marginBottom: "1rem" }} width="3840" height="2160" data-path="images/platform/developers/webhook-logs.png" />

Each row shows the status (success, failed, pending), the event type, the HTTP response code your endpoint returned, the response time, which attempt it was, the delivery ID and when it was sent. The page loads 50 attempts by default and can show more; the API caps a single request at 500.

The delivery ID also travels with the request as `X-Webhook-Delivery`, so a line in your own logs can be matched to a row here.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Saving the URL fails">
    The endpoint must use `https`, the hostname must resolve, and it must not point at a private or loopback address. A local tunnel with a public https hostname works; `http://localhost` does not.
  </Accordion>

  <Accordion title="Nothing arrives">
    Check the subscription is **active**, that it is subscribed to the event you expect, and run the test delivery. If the test succeeds but real events do not appear, the action you are testing may not be one of the eighteen events above.
  </Accordion>

  <Accordion title="Deliveries show as failed with no response code">
    The request never completed: DNS, TLS or a timeout. Raise **Timeout (seconds)** if your handler does slow work, or better, acknowledge with `200` first and process afterwards.
  </Accordion>

  <Accordion title="The same event arrives twice">
    Expected. Retries and at-least-once delivery mean duplicates happen. Deduplicate on the payload `id`.
  </Accordion>

  <Accordion title="The signature never matches">
    Three usual causes, in order. Your framework parsed the body before you signed it, so you are hashing re-serialised JSON instead of the bytes that arrived. You hashed the body alone instead of `<t>.<body>`. Or the secret in your configuration is not character for character the one saved on the subscription, which you can settle by saving a new secret in both places at once.

    If you are checking a receiver written before August 2026, it is comparing the old checksum format and will never match the new header.
  </Accordion>

  <Accordion title="You need an event that is not listed">
    Poll the relevant endpoint on a schedule, or build the flow in [n8n](/docs/platform/integrations). The event list is fixed.
  </Accordion>
</AccordionGroup>

***

## Related

* [Webhooks reference](/docs/api-reference/concepts/webhooks) - payload envelope, headers and event schemas
* [Handle webhooks](/docs/api-reference/recipes/handle-webhooks) - a full receiver with idempotency and replay
* [API Access](/docs/platform/api) - tokens for pulling data the other way
* [Integrations](/docs/platform/integrations) - HubSpot, n8n, analytics and SSO
* [OAuth Server](/docs/platform/oauth) - sign-in for your own applications
