Developers

HelpRange webhooks

Signed, retried JSON events for documents, file requests, data rooms, NDAs and offers.

Webhooks

HelpRange sends a signed JSON POST to your https endpoint the moment something happens: a document is opened, a file arrives, someone asks to join a data room, accepts an NDA or answers an offer. Set the endpoint in Settings → Integrations → Webhook (team administrators). Zapier “Catch Hook”, Make, n8n and your own server all work.

Events

EventWhen
document.openedSomeone opened a shared link.
file_request.uploadedA file came in through a file request.
room.access_requestedA verified visitor asks to join a data room.
nda.acceptedA visitor accepted (or signed) the NDA of a link or data room.
document.acknowledgedA data room guest confirmed reading a document.
offer.acceptedA reader accepted an offer.
offer.declinedA reader declined an offer.
offer.contact_requestedA reader asked to be contacted about an offer or document.

Payload

Every event has the same envelope. data depends on the event; new fields may be added at any time, so ignore fields you do not know. id is unique per event – use it to ignore duplicates.

{
  "id": "evt_3f9c2b7e0d1a4c5b8e6f7a9d0c1b2e3f",
  "event": "document.opened",
  "api_version": "2026-11-15",
  "created_at": "2026-11-15T09:12:03.512345Z",
  "data": {
    "document": "Offer 2026", "file_id": 123, "link": "ACME", "link_id": 456,
    "viewer": "jane@acme.com", "country": "PL", "browser": "Chrome", "os": "Mac OS X", "opening_id": 789
  }
}

Other data examples:

file_request.uploaded   {"request": "Financials 2025", "directory": "Due diligence", "filename": "balance.pdf",
                         "uploader_name": "Jan Kowalski", "uploader_email": "jan@example.com", "upload_id": 42}
room.access_requested   {"room": "Project Falcon", "room_id": 7, "request_id": 31, "email": "cfo@buyer.com",
                         "name": "Anna Buyer", "company": "Buyer Ltd", "message": "Please add me.", "review_url": "https://…"}
nda.accepted            {"nda": "Mutual NDA", "version": 2, "opened": "Project Falcon", "room_id": 7, "link_id": null,
                         "name": "Anna Buyer", "company": "Buyer Ltd", "email": "cfo@buyer.com", "method": "click",
                         "text_sha256": "…", "signed_sha256": "", "sealed": true, "certificate_url": "https://…"}
document.acknowledged   {"document": "Policy.pdf", "file_id": 123, "version": "3", "room": "Onboarding", "room_id": 7,
                         "email": "new.hire@acme.com", "response_id": 55}
offer.accepted          {"document": "Offer 2026", "file_id": 123, "link_id": 456, "folder_id": null,
offer.declined           "email": "bob@corp.com", "email_verified": true, "comment": "…", "engagement": 87,
offer.contact_requested  "response_id": 56, "offer_id": 9}

Test events sent from HelpRange carry "test": true.

Headers

HelpRange-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256> – see below. Right after a secret rotation there are two v1= values (new and old secret) for 24 hours.
HelpRange-Event-IdThe event's id. The same on every retry and resend.
HelpRange-Event-TypeThe event name, e.g. document.opened.
HelpRange-Delivery-Attempt1 for the first attempt, 2 for the first retry, …
X-HelpRange-SignatureLegacy: hex HMAC-SHA256 of the raw body only (no timestamp). Kept for existing integrations – prefer HelpRange-Signature.

Verify the signature

Your signing secret (whsec_…) is shown once when the endpoint is saved or the secret is rotated. To verify a request:

  1. Take t and every v1 from the HelpRange-Signature header.
  2. Compute HMAC-SHA256 with your secret over t + "." + the raw request body (the exact bytes – not re-serialised JSON).
  3. Accept the request if any v1 equals your hex digest (constant-time compare) and t is within 5 minutes of your clock – this stops replayed requests.

Node.js (Express):

const crypto = require('crypto');

function verifyHelpRange(rawBody, header, secret, toleranceSec = 300) {
  const parts = (header || '').split(',').map((p) => p.split('='));
  const t = (parts.find(([k]) => k === 't') || [])[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(t + '.').update(rawBody).digest('hex');
  return parts.some(([k, v]) => k === 'v1' && v.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
}

app.post('/helprange-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyHelpRange(req.body, req.get('HelpRange-Signature'), process.env.HELPRANGE_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // already seen event.id? answer 200 and skip it
  res.sendStatus(200);   // answer fast, do slow work in the background
});

Python:

import hashlib, hmac, time

def verify_helprange(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = [p.split('=', 1) for p in (header or '').split(',') if '=' in p]
    t = next((v for k, v in parts if k == 't'), None)
    if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
    return any(k == 'v1' and hmac.compare_digest(v, expected) for k, v in parts)

# Flask: verify_helprange(request.get_data(), request.headers.get('HelpRange-Signature'), SECRET)
# Django: verify_helprange(request.body, request.headers.get('HelpRange-Signature'), SECRET)

Test vector: secret whsec_test, t=1700000000, body {"id": "evt_test", "event": "document.opened"} → v1=7448beb8cec717ca314e71edd0a5076074ae36cf3d7eb4d68d076f178765e3f8.

Retries and delivery

  • Any 2xx answer within 10 seconds (5 s to connect) counts as delivered. Redirects are not followed.
  • Anything else – an error code, a timeout, a refused connection – is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h and 24 h: 9 attempts over about 3 days, then the event is marked failed.
  • Delivery is at-least-once: the same event can arrive twice. Deduplicate on id (HelpRange-Event-Id). Events can arrive out of order – use created_at.
  • Answer 410 Gone and HelpRange turns the endpoint off at once. After 15 failed attempts in a row over at least 24 hours it is turned off as well, and your team administrators get an e-mail. Turn it back on in the delivery log.
  • Only public https addresses are called.

Delivery log, test events and resend

Settings → Integrations → Delivery log lists every delivery of the last 30 days with status, attempts, response code and payload; filter by status, Resend any event (same event id) or Send test event. Rotate signing secret shows a new secret once; the old one keeps signing for 24 hours so you can switch without losing events.