# Receive webhooks

Have your project call your server when a person is created, a session ends or a sign-in fails, and verify that each call is genuine.

A webhook is an HTTP call from your project to your server when something happens. Use webhooks to keep another system current: create a workspace when a person is created, or close a person's sessions elsewhere when they are suspended.

## Before you begin

You need:

- An `https` URL on your server. The URL must be public: a webhook cannot be pointed at a private or internal address.
- A handler at that URL that can read the raw request body and answer within 10 seconds.
- Access to **Integrations > Connections** in the AuthFI console.

<Steps>

### Register the webhook

In the console, open **Integrations > Connections** and choose **Connect something**. Under **Webhooks**, choose **Custom webhook**. The **Register a webhook** dialog opens.

| Field        | Enter                                                                                |
| ------------ | ------------------------------------------------------------------------------------ |
| Delivery URL | The `https` URL on your server                                                       |
| Events       | **All events**, or **Choose events** and the events you want. See [Events](#events). |

Choose **Register**.

### Store the signing secret

The dialog now shows the signing secret, under the title **Its signing secret, shown once**. Copy the secret and store it with your server's other secrets. The signing secret is how you tell a genuine call from a forged one.

Choose **I've stored it**. The webhook appears as a card on the **Connections** tab.

### Verify the signature

Every call carries a signature in the `X-AuthFI-Signature-256` header. The signature is an HMAC-SHA256, keyed with your signing secret, of three parts joined together: the value of `X-AuthFI-Timestamp`, a dot, and the exact body you received.

Compute the same value on your server and compare the two.

<Tabs labels={["Node.js", "Python"]}>

```js
import { createHmac, timingSafeEqual } from "node:crypto";

// `rawBody` is the bytes as received, before any JSON parsing.
export function genuine(rawBody, headers, secret) {
  const timestamp = headers["x-authfi-timestamp"];
  const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
  const given = headers["x-authfi-signature-256"] ?? "";
  return expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
```

```python
import hashlib, hmac

# `raw_body` is the bytes as received, before any JSON parsing.
def genuine(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers["X-AuthFI-Timestamp"]
    expected = "sha256=" + hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, headers.get("X-AuthFI-Signature-256", ""))
```

</Tabs>

`secret` is the signing secret you stored. `headers` are the request headers, and the body is the bytes as they arrived.

Check the signature before you do anything else. Reject a call whose signature does not match, and reject a timestamp more than a few minutes old.

### Answer within 10 seconds

Answer with any `2xx` status within 10 seconds. Do the work afterwards: accept the call, queue it, and return.

</Steps>

## Check that it works

Cause one of the events you chose. For example, with **Group member added** selected, add a person to a group in the console.

Your server receives a `POST` whose `X-AuthFI-Event` header is `group.member_added`, and your signature check passes.

## Events

| Event                  | Label in the console | When                                              |
| ---------------------- | -------------------- | ------------------------------------------------- |
| `user.created`         | User created         | A person was added                                |
| `user.updated`         | User updated         | A person's details changed                        |
| `user.deleted`         | User deleted         | A person was removed                              |
| `user.blocked`         | User blocked         | A person was suspended                            |
| `user.mfa_enrolled`    | User enrolled MFA    | A person set up multi-factor authentication (MFA) |
| `login.success`        | Login succeeded      | Someone signed in                                 |
| `login.failed`         | Login failed         | A sign-in failed                                  |
| `session.revoked`      | Session revoked      | A session was ended                               |
| `group.member_added`   | Group member added   | A person joined a group                           |
| `group.member_removed` | Group member removed | A person left a group                             |
| `organization.created` | Organization created | An organization was created                       |
| `role.assigned`        | Role assigned        | A role was given to someone                       |

## What your server receives

Each call is a `POST` with a JSON body.

```json
{
  "id": "9b2f6c1e-...",
  "event": "group.member_added",
  "timestamp": "2026-10-10T09:14:07Z",
  "data": {
    "actor": "user:grace@acme.com",
    "subject": "user:ada@acme.com",
    "resource_type": "group",
    "resource_id": "engineering"
  }
}
```

The call carries these headers:

| Header                   | Value                                             |
| ------------------------ | ------------------------------------------------- |
| `X-AuthFI-Event`         | The event name                                    |
| `X-AuthFI-Delivery`      | An ID for this delivery. The same across retries. |
| `X-AuthFI-Timestamp`     | When it was sent                                  |
| `X-AuthFI-Signature-256` | `sha256=` and the signature, in hex               |

## How deliveries are retried

Your answer decides whether AuthFI sends the call again.

| Your answer                                     | What happens                                                                              |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `2xx`                                           | Delivered                                                                                 |
| `5xx`, `408`, `429`, or no answer in 10 seconds | Sent up to 6 times in all: the first attempt, then again after 2, 4, 8, 16 and 32 minutes |

A retry carries the same `X-AuthFI-Delivery` as the first attempt. Store the IDs you have handled, and ignore a delivery you have already handled.

## Order of deliveries

Deliveries can arrive in any order. When order matters, sort by `timestamp` in the body.

## Pause or remove a webhook

On **Integrations > Connections**, open the **Connections** tab and find the webhook's card.

- To stop deliveries for a while, choose **Pause** and confirm with **Pause deliveries**. To start them again, choose **Resume** and confirm with **Resume deliveries**.
- To remove the webhook, choose **Disconnect** and confirm with **Disconnect**.

## Troubleshooting

| What you see                          | Cause                                                               | Fix                                                    |
| ------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------ |
| The same delivery arrives again       | Your server answered `5xx`, `408` or `429`, or took over 10 seconds | Answer `2xx` first and do the work afterwards          |
| One event is handled twice            | A retry was treated as a new delivery                               | Ignore an `X-AuthFI-Delivery` you have already handled |
| The signature never matches           | The body was parsed or changed before the check                     | Compute the HMAC over the bytes as received            |
| Events are handled in the wrong order | Deliveries can arrive in any order                                  | Sort by `timestamp`                                    |

## Next steps

<Cards>
  <Card href="/extend/actions" title="Run actions at sign-in">
    Add claims to a token, or decide who gets in, while a person signs in.
  </Card>
  <Card href="/security/audit" title="Use the audit record">
    Send every event in the project to your log tools.
  </Card>
  <Card href="/agents/revocation" title="Stop an agent">
    Receive a signed event when an AI agent is revoked.
  </Card>
  <Card href="/security/keys" title="Keys and secrets">
    How secrets that are shown once are kept.
  </Card>
</Cards>
