# Webhooks

> Webhooks send a signed HTTP request to another system whenever records in your tables are created, updated, or deleted. Learn how to set one up.

A webhook tells another system about changes in SAPUGO. Whenever a record in one of the chosen tables is created, updated, or deleted, SAPUGO sends an HTTP POST with the details to an address you choose. Each request is signed, so the receiver can check that it really came from you.

Use a webhook to keep another system in step with every change, such as a data warehouse, a shop, or your own service.

## Webhook or workflow

| You need | Use |
| --- | --- |
| Tell another system about every change to a table, as it happens | A webhook |
| Call another system only in some cases, or with a payload in its own format | A [workflow](/docs/workflows) with an [HTTP step](/docs/workflows/nodes/other-systems#web-apis) |
| Stop a bad record from being saved | A [validation](/docs/business-rules/validation) or [guard](/docs/business-rules/guard) |

A webhook fires after the record has been saved, so it can't refuse or change a save.

## Create a webhook

1. Open **Settings**, choose **Developer Tools**, then **Webhooks**.
2. Click **New webhook**.
3. Enter a **Name**, such as "Customer sync", and optionally a description.
4. Enter the **Target URL** that should receive the requests. It must be reachable from the internet and use HTTPS. Private and local network addresses are rejected.
5. Pick the **Database**, then the **Tables** whose changes should be sent, up to twenty.
6. Under **Events**, choose CREATE, UPDATE, DELETE, or **All events**.
7. Make sure **Active** is on, and save.
8. Copy the signing secret from **Save your signing secret** and store it with your receiver. It's shown only once.

> **Warning:** The signing secret is never shown again after you close that window. If you lose it, use **Rotate** to create a new one.

To try it out, click **Test**, choose the **Table to test**, and click **Send test event**. The test request has a placeholder record with `test` set to `true`, so your receiver can tell it apart from real changes.

## What SAPUGO sends

Each change is sent as a POST request with a JSON body.

```json
{
  "event_id": "4f1c2d3e-…",
  "event_type": "UPDATE",
  "timestamp": 1760356800000,
  "org_id": "…",
  "database_id": "…",
  "table_id": "…",
  "table_name": "customers",
  "row_id": "…",
  "field_name": "status",
  "old_value": "lead",
  "new_value": "customer",
  "new_row": { "_id": "…", "name": "Acme Ltd", "status": "customer" },
  "created_by": "…",
  "subscription_id": "…",
  "delivery_id": "…",
  "resend_count": 0
}
```

| Field | Meaning |
| --- | --- |
| `event_type` | `CREATE`, `UPDATE`, or `DELETE`. |
| `event_id` | A unique ID for the change. Use it to ignore duplicates. |
| `table_name`, `table_id`, `database_id` | Where the change happened. |
| `row_id` | The record that changed. |
| `field_name`, `old_value`, `new_value` | The field that changed and its values before and after, when the change concerns a single field. |
| `new_row` | The record as it is after the change. |
| `created_by` | Who made the change. |
| `delivery_id`, `subscription_id` | This delivery and the webhook it belongs to. |
| `resend_count` | How many times this delivery has been resent by hand. |

Each request also carries these headers: `X-Sapugo-Signature`, `X-Sapugo-Delivery-Id`, `X-Sapugo-Subscription-Id`, `X-Sapugo-Attempt`, `X-Sapugo-Event-Type`, `X-Sapugo-Event-Id`, and `X-Sapugo-Timestamp`.

## Check the signature

Before trusting a request, check that it was signed with your secret. The `X-Sapugo-Signature` header has the form `sha256=` followed by a hex-encoded HMAC-SHA256 of the raw request body, using the signing secret as the key.

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

function isValid(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    expected.length === signatureHeader.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
  );
}
```

Compute the signature over the body exactly as it arrived, before parsing the JSON.

## Delivery and retries

- Reply with any 2xx status to confirm a delivery. Reply quickly and do slow work afterwards, because a request that takes too long counts as failed.
- Failed deliveries are retried automatically. Server errors, timeouts, and the statuses 408, 425, and 429 are retried. Other 4xx responses are not.
- A delivery can arrive more than once. Use `event_id` to skip events you've already handled.
- After 20 failures in a row, the webhook is switched off and shows **Auto-disabled**. Fix the receiver, then turn the webhook back on.

## Delivery logs

The **Delivery logs** tab lists every delivery with its status, event, number of attempts, and duration. Open one to see the request payload, the response, and a timeline of attempts.

| Status | Meaning |
| --- | --- |
| **Pending** | Waiting to be sent. |
| **Delivering** | Being sent now. |
| **Delivered** | Your receiver confirmed it. |
| **Retrying** | An attempt failed and another is scheduled. |
| **Failed** | The delivery failed. |
| **Dead lettered** | All retries are used up. |
| **Resent** | The delivery was sent again by hand. |

Click **Resend** on a delivery to send the original payload again, for example after fixing a bug in your receiver.

## Manage webhooks

- **Rotate** creates a new signing secret. Receivers using the old secret start failing until you update them.
- **Archive** stops a webhook for good. Its delivery history is kept.
- Edit a webhook to change its tables, events, or address, or to switch **Active** off for a while.

> **Note:** Webhooks are included on selected plans, and you need permission to manage them. Webhooks aren't available on time series tables. Each payload can be up to 256 KB.

  - [Workflows](/docs/workflows): Call other systems with your own logic.
  - [Other systems steps](/docs/workflows/nodes/other-systems): HTTP, FTP, EDI, and AI steps.

---

Source: https://community.sapugo.app/docs/webhooks
