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.

DocumentEvent names
Electronic bill of ladingebl.issued, ebl.transferred, ebl.surrendered
Bill of exchangeboe.issued, boe.transferred, boe.surrendered
Every other type, including promissory notes and the records you file with POST /ebldocument.issued, document.transferred, document.surrendered
  • The bare names issued, transferred and surrendered match every type.
  • Only transferable records are ever transferred or surrendered. An endorsement, which changes the owner, arrives as a transferred event.
  • A subscription to the ebl. names also receives the events of records filed with POST /ebl. Their event field still says document..
  • 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.verified or the member. and apikey. 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-Delivery or data.eventId to 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.