Preview. This API will change in v1.0.0. Breaking changes are announced in the release notes with migration steps before they apply, unless a security or legal issue needs an immediate change; then we announce it there as soon as we can.
Webhooks
DocuShield sends a signed POST to your URL when a document in your organisation is issued, transferred or surrendered.
Subscribe
Create a webhook with POST /webhooks and a body of { "url": "https://...", "events": [...] }. The URL must use https and must not point to a private or internal network address. The response includes a signing secret, shown once.
| Document | Event names |
|---|---|
| Electronic bill of lading | ebl.issued, ebl.transferred, ebl.surrendered |
| Bill of exchange | boe.issued, boe.transferred, boe.surrendered |
Every other type, including promissory notes and the records you file with POST /ebl | document.issued, document.transferred, document.surrendered |
- The bare names
issued,transferredandsurrenderedmatch every type. - Only transferable records are ever transferred or surrendered. An endorsement, which changes the owner, arrives as a
transferredevent. - A subscription to the
ebl.names also receives the events of records filed withPOST /ebl. Theireventfield still saysdocument.. - DocuShield does not check the names you subscribe with, so a misspelt name is accepted and never matches.
- The Webhooks page in the app lists more event names than these. DocuShield never sends events such as
document.verifiedor themember.andapikey.names.
A webhook receives only events that happen after it was created. It receives the events of sandbox and production records alike; see Authentication.
What arrives
{
"event": "ebl.transferred",
"created": "2026-09-28T10:15:00.123456+00:00",
"orgId": "...",
"data": {
"id": "...", "eventId": "...", "blNumber": "...", "documentType": "ebl",
"lifecycleState": "transferred", "currentHolder": "0x...", "txHash": "0x...",
"fromParty": "0x...", "toParty": "0x...", "transferType": "transferHolder"
}
}
A field with no value is left out. transferType names the on-chain action: transferHolder, transferBeneficiary, transferOwners or surrender.
Headers: X-DocuShield-Signature, X-DocuShield-Event (the same name as the event field) and X-DocuShield-Delivery (the id of this delivery).
Check the signature
X-DocuShield-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your secret. Compute it over the exact bytes you received, before parsing the JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromDocuShield(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib
import hmac
def is_from_docushield(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), (signature_header or "").encode())
The signature has no timestamp and does not cover the headers, so it cannot stop someone from sending you a message they captured again. Keep the data.eventId values you have handled, which the signature does cover, and ignore repeats.
Delivery
- A background job sends deliveries, not the request that caused the event, so a message can arrive a little after the event.
- Reply with any 2xx status within 10 seconds. Redirects are not followed and count as a failure.
- Delivery is at least once. Use
X-DocuShield-Deliveryordata.eventIdto ignore a message you have already handled: each event reaches a webhook in one delivery, so both identify it. - A delivery gets up to 5 attempts in total, with pauses of 0.5, 1, 2 and 4 seconds between them. After the fifth failed attempt it is marked failed. The Preview has no later retry and no replay.