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

# Configure Webhooks to Receive Real-Time Event Notifications

> Set up a webhook endpoint to receive real-time notifications from the Idem Ledger API. Covers registration, secret verification, and updating your URL.

Webhooks push event notifications from the Idem Ledger directly to your server the moment something happens — a transaction commits, a settlement matches, or an expectation goes unmatched. Rather than polling the API to check for state changes, you configure a single HTTPS endpoint for your tenant and let Idem deliver events to you in real time.

## Register your webhook

Send a `PUT` request to `/api/v1/tenant/webhook` with the URL you want Idem to deliver events to. This endpoint both registers a new webhook and replaces an existing one.

```bash theme={null}
curl -X PUT https://api.your-domain.com/api/v1/tenant/webhook \
  -H "Authorization: Bearer $IDEM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl": "https://your-app.com/webhooks/idem"}'
```

The response includes a **webhook secret**. This secret is used to generate HMAC signatures on outgoing requests so you can verify that events genuinely originate from Idem.

```json theme={null}
{
  "webhookUrl": "https://your-app.com/webhooks/idem",
  "webhookSecret": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}
```

<Note>
  The secret is a raw 64-character hex string with no prefix — do not assume a fixed prefix like `whsec_` when parsing or validating it.
</Note>

<Warning>
  The webhook secret is returned **only once** — at the moment of registration or update. Copy it immediately and store it in a secure secrets manager (such as AWS Secrets Manager, HashiCorp Vault, or your platform's equivalent). If you lose the secret, you must update your webhook URL to trigger a new secret issuance; the old secret cannot be retrieved.
</Warning>

## Retrieve your webhook config

To check which URL is currently registered for your tenant, send a `GET` request to `/api/v1/tenant/webhook`. The secret is masked in this response to protect it from accidental exposure.

```bash theme={null}
curl https://api.your-domain.com/api/v1/tenant/webhook \
  -H "Authorization: Bearer $IDEM_API_KEY"
```

```json theme={null}
{
  "webhookUrl": "https://your-app.com/webhooks/idem",
  "secretPrefix": "a1b2c3d4..."
}
```

If no webhook has been configured for your tenant yet, the API returns `404 Not Found`. Use the `PUT` endpoint described above to register one.

## Verifying webhook signatures

Every request Idem sends to your endpoint includes an HMAC signature in the `X-Idem-Signature` header, formatted as `sha256=<hex-digest>`. You must verify this signature before processing the event payload — it confirms the request came from Idem and has not been tampered with in transit.

The general verification flow is:

1. Extract the raw request body (as bytes, before any JSON parsing).
2. Extract the `X-Idem-Signature` header value and strip the `sha256=` prefix.
3. Compute an HMAC-SHA256 digest of the raw body using your stored webhook secret as the key.
4. Compare your computed digest against the signature header value using a constant-time comparison.
5. Reject any request where the signatures do not match.

<CodeGroup>
  ```js Node.js theme={null}
  const crypto = require("crypto");

  function verifySignature(rawBody, signatureHeader, secret) {
    if (!signatureHeader?.startsWith("sha256=")) return false;
    const received = signatureHeader.slice("sha256=".length);

    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody) // Buffer of the raw, unparsed request body
      .digest("hex");

    const receivedBuf = Buffer.from(received, "hex");
    const expectedBuf = Buffer.from(expected, "hex");
    if (receivedBuf.length !== expectedBuf.length) return false;

    return crypto.timingSafeEqual(receivedBuf, expectedBuf);
  }
  ```

  ```kotlin Kotlin theme={null}
  import java.security.MessageDigest
  import javax.crypto.Mac
  import javax.crypto.spec.SecretKeySpec

  fun verifySignature(rawBody: ByteArray, signatureHeader: String?, secret: String): Boolean {
      val received = signatureHeader?.removePrefix("sha256=") ?: return false

      val mac = Mac.getInstance("HmacSHA256")
      mac.init(SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256"))
      val expected = mac.doFinal(rawBody).joinToString("") { "%02x".format(it) }

      // MessageDigest.isEqual is constant-time, unlike String.equals
      return MessageDigest.isEqual(
          received.toByteArray(Charsets.UTF_8),
          expected.toByteArray(Charsets.UTF_8),
      )
  }
  ```

  ```java Java theme={null}
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import javax.crypto.Mac;
  import javax.crypto.spec.SecretKeySpec;

  public class WebhookSignature {
      public static boolean verifySignature(byte[] rawBody, String signatureHeader, String secret) throws Exception {
          if (signatureHeader == null || !signatureHeader.startsWith("sha256=")) {
              return false;
          }
          String received = signatureHeader.substring("sha256=".length());

          Mac mac = Mac.getInstance("HmacSHA256");
          mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
          byte[] digest = mac.doFinal(rawBody);

          StringBuilder expected = new StringBuilder();
          for (byte b : digest) {
              expected.append(String.format("%02x", b));
          }

          // MessageDigest.isEqual is constant-time, unlike String.equals
          return MessageDigest.isEqual(
              received.getBytes(StandardCharsets.UTF_8),
              expected.toString().getBytes(StandardCharsets.UTF_8)
          );
      }
  }
  ```

  ```bash OpenSSL (manual check) theme={null}
  # Quick manual check against a captured payload — not for use inside a handler.
  echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
  ```
</CodeGroup>

In each of these, `rawBody` must be the exact, unparsed request body bytes — if your framework's body parser (e.g. Express's `express.json()`) has already deserialized the payload by the time your handler runs, capture the raw bytes separately (for example, with `express.raw({ type: "application/json" })` on the webhook route) before any JSON parsing happens.

Reject and do not process any webhook delivery where signature verification fails.

## Update your webhook URL

If you need to rotate your endpoint — for example, after a domain migration or a security rotation — simply `PUT` to `/api/v1/tenant/webhook` again with the new URL:

```bash theme={null}
curl -X PUT https://api.your-domain.com/api/v1/tenant/webhook \
  -H "Authorization: Bearer $IDEM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl": "https://your-new-app.com/webhooks/idem"}'
```

Updating the URL issues a **new secret**. Store the new secret immediately and update your application configuration before the old endpoint stops accepting traffic. Events will be delivered to the new URL as soon as the update is confirmed.

## Required scope

Your API key must have the **WEBHOOK\_MANAGE** scope to call either the `GET` or `PUT` endpoints. Only privileged administrative keys should hold this scope to limit who can redirect event delivery.
