{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-products/leandisposable/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"type":"markdown"},"seo":{"title":"Webhook configuration","description":"Integration and usage documentation.","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhook-configuration","__idx":0},"children":["Webhook configuration"]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"danger"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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 ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/products/leandisposable/enterprise-documentation/developer-documentation/release-notes"},"children":["Release Notes"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Example in the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/products/leandisposable/enterprise-documentation/developer-documentation/api-references/lean-documents/lean-uploaddoc-method"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["uploadDoc"]}]}," method:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"webhookUrl\": \"https://example.com/callback\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The webhook settings include the following parameter."]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["webhookUrl"]},": The destination URL where the webhook notification will be sent.",{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Must be a valid, publicly accessible URL"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["HTTPS is required, on port 443"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The host must be a registered name — IP literals are rejected"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The URL must not embed credentials (no userinfo component before the host)"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Maximum length 2048 characters"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The host must be ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["allowlisted"]}," for your account — see ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"#host-allowlist"},"children":["Host allowlist"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Validation is synchronous: a URL that does not satisfy these rules makes the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["uploadDoc"]}," call fail"," ","with HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," and a dedicated application error code, so the problem surfaces at integration time"," ","rather than as silently undelivered notifications."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The webhook URL is defined per document, inline in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["uploadDoc"]}," request. There is no global"," ","reusable configuration, and a document may have at most one webhook endpoint."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For details on notification payloads, retry behavior, and status transitions, see"," ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/products/leandisposable/enterprise-documentation/developer-documentation/wb/wb-how"},"children":["Understanding webhook"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"host-allowlist","__idx":1},"children":["Host allowlist"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"verify-jws-signatures","__idx":2},"children":["Verify JWS signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Namirial signs every outgoing notification and attaches the signature as an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Namirial-Signature"]}," ","request header. This allows you to verify the authenticity and integrity of every notification."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The request headers of a notification are:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"POST <webhookUrl>\nContent-Type: application/json\nX-Namirial-Signature: <compact JWS, detached payload>\nX-Namirial-Event-Id: <eventId>\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The request body is sent in plain text. The signature is a JWS with ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["detached payload"]}," ","(",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://datatracker.ietf.org/doc/html/rfc7797"},"children":["RFC 7797"]},"): the payload part of the token is empty"," ","and the signed content is the raw request body."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"X-Namirial-Signature: <header>..<signature>\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The protected header, once base64url-decoded, looks like this:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"alg\": \"RS256\",\n  \"b64\": false,\n  \"crit\": [\"b64\"],\n  \"x5c\": [\"<leaf certificate, DER, Base64>\", \"<intermediate, DER, Base64>\"],\n  \"x5t#S256\": \"<SHA-256 thumbprint of the leaf certificate>\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Field"},"children":["Field"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["alg"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The signing algorithm — always ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RS256"]}," (RSASSA-PKCS1-v1_5 using SHA-256, as described in ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://datatracker.ietf.org/doc/html/rfc7518#section-3.1"},"children":["RFC7518 §3.1"]},")"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["b64"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Always ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["false"]}," — the payload is detached and not base64url-encoded"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["crit"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Always ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["[\"b64\"]"]}," — a verifier that does not understand ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["b64"]}," must reject the token"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x5c"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The certificate chain, leaf first then intermediates. ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["The root is not included"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x5t#S256"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["SHA-256 thumbprint of the leaf certificate — use it to cache the chain validation result across notifications"]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To verify a notification, follow these steps."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Step 1 — Validate the certificate chain"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Decode the JWS header (the first part of the token, before the first ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["."]},") from base64url to JSON and"," ","read the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x5c"]}," array."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Validate the chain up to the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Never trust the embedded chain on its own. An attacker can sign a forged notification with their own"," ","certificate and embed that certificate in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x5c"]},". Only a chain that terminates in the pinned Namirial"," ","root proves the origin of the notification."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Then verify that:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["the leaf subject / SAN matches the value communicated at onboarding"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["the leaf validity period covers the current time"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["the leaf is not revoked (CRL or OCSP)"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If any of these checks fails, the notification must be rejected without verifying the signature."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Step 2 — Rebuild the signed input"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Because the payload is detached, the signed input is not carried in the token. Rebuild it as:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"signed_input = <header> + \".\" + <raw request body>\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["where ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<header>"]}," is the first part of the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Namirial-Signature"]}," value, exactly as received (still"," ","base64url-encoded), and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<raw request body>"]}," is the request body ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["byte for byte"]},", with no"," ","re-serialization, no whitespace normalization and no key reordering."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Step 3 — Verify the signature"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Using the algorithm specified in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["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"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["signed_input"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If the verification fails, the notification must be rejected."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"token   = request.headers[\"x-namirial-signature\"]\nparts   = token.split(\".\")          // parts[1] is empty: detached payload\n\nheader    = base64url_decode(parts[0])\nsignature = base64url_decode(parts[2])\n\nchain = header[\"x5c\"]\nassert validate_chain(chain, pinned_namirial_root)   // step 1\nassert leaf_not_revoked(chain[0])\n\nsigned_input = parts[0] + \".\" + raw_request_body\npublic_key   = public_key_of(chain[0])\n\nassert rsa_sha256_verify(public_key, signed_input, signature)\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Step 4 — Deduplicate and answer"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Delivery is at-least-once. Deduplicate on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["eventId"]}," field of the payload (also available in the"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Namirial-Event-Id"]}," header) and answer HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," once the event has been accepted."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Any status other than ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," — including ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["202"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["204"]}," — causes redelivery for up to 24 hours. See"," ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/products/leandisposable/enterprise-documentation/developer-documentation/wb/wb-how#webhook-retry-mechanism"},"children":["Webhook Retry Mechanism"]},"."]}]}]},"headings":[{"value":"Webhook configuration","id":"webhook-configuration","depth":1},{"value":"Host allowlist","id":"host-allowlist","depth":2},{"value":"Verify JWS signatures","id":"verify-jws-signatures","depth":2}],"frontmatter":{"seo":{"title":"Webhook configuration"}},"lastModified":"2026-08-31T14:09:54.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/products/leandisposable/enterprise-documentation/developer-documentation/wb/wb-configuration","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}