# Webhook configuration

**DRAFT — not yet available.** This page describes a feature whose development has not started
yet. Everything below — field names, payload structure, headers and behaviour — may change
before release, and the functionality is not available in any environment. Do not implement
against it. This page will lose the DRAFT notice when the feature ships; the release it belongs
to will be listed in the [Release Notes](/products/leandisposable/enterprise-documentation/developer-documentation/release-notes).

Each document uploaded through Lean Disposable can be linked to a webhook URL, so that the
preservation notification is sent to the endpoint you designate.

Example in the [`uploadDoc`](/products/leandisposable/enterprise-documentation/developer-documentation/api-references/lean-documents/lean-uploaddoc-method) method:


```json
{
  "webhookUrl": "https://example.com/callback"
}
```

The webhook settings include the following parameter.

- **webhookUrl**: The destination URL where the webhook notification will be sent.
  - Must be a valid, publicly accessible URL
  - HTTPS is required, on port 443
  - The host must be a registered name — IP literals are rejected
  - The URL must not embed credentials (no userinfo component before the host)
  - Maximum length 2048 characters
  - The host must be **allowlisted** for your account — see [Host allowlist](#host-allowlist)


Validation is synchronous: a URL that does not satisfy these rules makes the `uploadDoc` call fail
with HTTP `400` and a dedicated application error code, so the problem surfaces at integration time
rather than as silently undelivered notifications.

The webhook URL is defined per document, inline in the `uploadDoc` request. There is no global
reusable configuration, and a document may have at most one webhook endpoint.

For details on notification payloads, retry behavior, and status transitions, see
[Understanding webhook](/products/leandisposable/enterprise-documentation/developer-documentation/wb/wb-how).

## Host allowlist

Notification destinations are restricted to an allowlist of hosts, denied by default. The allowlist
is populated by Namirial during onboarding: there is no API to register your own host, since that
would defeat the control.

Provide the hosts you intend to use to your Namirial contact before going live. The host is also
re-validated at delivery time, so a host that is later disabled stops receiving notifications even
for documents already uploaded.

## Verify JWS signatures

Namirial signs every outgoing notification and attaches the signature as an `X-Namirial-Signature`
request header. This allows you to verify the authenticity and integrity of every notification.

The request headers of a notification are:


```
POST <webhookUrl>
Content-Type: application/json
X-Namirial-Signature: <compact JWS, detached payload>
X-Namirial-Event-Id: <eventId>
```

The request body is sent in plain text. The signature is a JWS with **detached payload**
([RFC 7797](https://datatracker.ietf.org/doc/html/rfc7797)): the payload part of the token is empty
and the signed content is the raw request body.


```
X-Namirial-Signature: <header>..<signature>
```

The protected header, once base64url-decoded, looks like this:


```json
{
  "alg": "RS256",
  "b64": false,
  "crit": ["b64"],
  "x5c": ["<leaf certificate, DER, Base64>", "<intermediate, DER, Base64>"],
  "x5t#S256": "<SHA-256 thumbprint of the leaf certificate>"
}
```

| Field | Description |
|  --- | --- |
| `alg` | The signing algorithm — always `RS256` (RSASSA-PKCS1-v1_5 using SHA-256, as described in [RFC7518 §3.1](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1)) |
| `b64` | Always `false` — the payload is detached and not base64url-encoded |
| `crit` | Always `["b64"]` — a verifier that does not understand `b64` must reject the token |
| `x5c` | The certificate chain, leaf first then intermediates. **The root is not included** |
| `x5t#S256` | SHA-256 thumbprint of the leaf certificate — use it to cache the chain validation result across notifications |


To verify a notification, follow these steps.

**Step 1 — Validate the certificate chain**

Decode the JWS header (the first part of the token, before the first `.`) from base64url to JSON and
read the `x5c` array.

Validate the chain up to the **Namirial root CA delivered to you at onboarding**. The root is
published on this documentation site together with its declared SHA-256 fingerprint, and it must be
pinned in your application.

Never trust the embedded chain on its own. An attacker can sign a forged notification with their own
certificate and embed that certificate in `x5c`. Only a chain that terminates in the pinned Namirial
root proves the origin of the notification.

Then verify that:

- the leaf subject / SAN matches the value communicated at onboarding
- the leaf validity period covers the current time
- the leaf is not revoked (CRL or OCSP)


If any of these checks fails, the notification must be rejected without verifying the signature.

**Step 2 — Rebuild the signed input**

Because the payload is detached, the signed input is not carried in the token. Rebuild it as:


```
signed_input = <header> + "." + <raw request body>
```

where `<header>` is the first part of the `X-Namirial-Signature` value, exactly as received (still
base64url-encoded), and `<raw request body>` is the request body **byte for byte**, with no
re-serialization, no whitespace normalization and no key reordering.

**Step 3 — Verify the signature**

Using the algorithm specified in `alg` and the public key of the leaf certificate validated in
step 1, verify that the signature (the third part of the token) is a valid signature of
`signed_input`.

If the verification fails, the notification must be rejected.


```
token   = request.headers["x-namirial-signature"]
parts   = token.split(".")          // parts[1] is empty: detached payload

header    = base64url_decode(parts[0])
signature = base64url_decode(parts[2])

chain = header["x5c"]
assert validate_chain(chain, pinned_namirial_root)   // step 1
assert leaf_not_revoked(chain[0])

signed_input = parts[0] + "." + raw_request_body
public_key   = public_key_of(chain[0])

assert rsa_sha256_verify(public_key, signed_input, signature)
```

**Step 4 — Deduplicate and answer**

Delivery is at-least-once. Deduplicate on the `eventId` field of the payload (also available in the
`X-Namirial-Event-Id` header) and answer HTTP `200` once the event has been accepted.

Any status other than `200` — including `202` and `204` — causes redelivery for up to 24 hours. See
[Webhook Retry Mechanism](/products/leandisposable/enterprise-documentation/developer-documentation/wb/wb-how#webhook-retry-mechanism).