Webhooks
Webhooks push real-time events to your endpoint — a new transaction, a completed extraction, a viewed disclosure — so you can react instead of polling. Send them to Zapier, Slack, your CRM, your own service, or any HTTPS URL. Want deal events in GoHighLevel or another CRM? See Send deal data to a CRM.
You can manage webhook subscriptions two ways:
- In DocJacket — Settings → Connections (create, edit, view delivery stats).
- Via the API — with your
mcp_at_key, for fully programmatic setup.
Manage subscriptions with the API
# List the event types you can subscribe to
curl https://api.docjacket.com/api/v1/webhook-events \
-H "Authorization: Bearer mcp_at_YOUR_KEY"
# Create a subscription (requires the `actions` scope)
curl -X POST https://api.docjacket.com/api/v1/webhooks \
-H "Authorization: Bearer mcp_at_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ops Slack relay",
"url": "https://example.com/hooks/docjacket",
"events": ["extraction.completed", "task.completed"]
}'
The create response includes the signing secret in full, exactly once — store it; you'll need it to verify signatures. Later reads only show a masked prefix. Other operations:
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/v1/webhooks | read | List your subscriptions |
| GET | /api/v1/webhooks/{id} | read | Get one subscription |
| POST | /api/v1/webhooks | actions | Create a subscription |
| PUT | /api/v1/webhooks/{id} | actions | Update url / events / active |
| DELETE | /api/v1/webhooks/{id} | actions | Delete a subscription |
| POST | /api/v1/webhooks/{id}/test | actions | Fire a test delivery now |
| POST | /api/v1/webhooks/{id}/regenerate-secret | actions | Rotate the signing secret |
| GET | /api/v1/webhooks/{id}/deliveries | read | Recent delivery attempts |
| GET | /api/v1/webhook-events | read | The event catalog |
Subscribe to "*" to receive every event type.
Event catalog
| Event | Fires when |
|---|---|
contact.created | When a new contact is created |
disclosure.recipient_viewed | When a disclosure package recipient opens the disclosure |
document.deleted | When a document is deleted |
document.uploaded | When a document is uploaded to a transaction |
extraction.completed | When a document extraction job finishes (includes jobId + status) |
form.submission_received | When a public form submission is received |
key_date.completed | When a key date is marked complete |
key_date.updated | When a key date is added or changed on a transaction |
task.completed | When a task is marked complete |
task.created | When a new task is created |
transaction.archived | When a transaction is archived |
transaction.closed | When a transaction moves into a closed status, by any route (includes agents, office, price, TC fee and closing date) |
transaction.created | When a new transaction is created |
transaction.deleted | When a transaction is deleted |
transaction.status_changed | When a transaction status changes (includes the deal snapshot) |
webhook.test | Test webhook (triggered manually) |
The same list is available at runtime from GET /api/v1/webhook-events, and in the OpenAPI specification under webhooks — where each event also carries its payload schema and an example.
extraction.completed turns a document upload into a round-trip: subscribe, upload via the API, and receive the signed callback with the jobId and confidence when extraction finishes — no polling.
Payload
Every delivery is a POST with a JSON body in this envelope:
{
"eventType": "extraction.completed",
"eventId": "b1c2…",
"timestamp": "2026-07-04T13:00:00Z",
"organizationId": "org-…",
"userId": null,
"data": {
"jobId": "…",
"transactionId": "…",
"documentId": "…",
"status": "completed",
"confidence": 0.98,
"requiresReview": false
}
}
The shape of data depends on the event. Most events are thin: they carry IDs and a few key fields, and you fetch the full resource from the REST API when you need more. The exception is the deal events (transaction.created, transaction.status_changed, transaction.closed), which also carry a full deal snapshot (address, status, price, closing date, TC fee, commissions, office, assignee, agents and custom fields), so you rarely need a follow-up call. Fields may be added over time; ignore ones you don't recognize.
Deal events
Here is a transaction.status_changed delivery:
{
"eventType": "transaction.status_changed",
"eventId": "3f2a91c7-5d64-4e18-9b03-8c7d1e4a6f52",
"timestamp": "2026-08-04T15:42:07.1183940Z",
"organizationId": "org-9fc8a1d2e3b4",
"userId": "1c9e6b40-72af-4d35-8e21-5b0a3f7c9d16",
"userName": "Jordan Rivera",
"data": {
"id": "8a4c2e10-9f5b-4713-b6d8-0e2a7c5f3149",
"oldStatus": "Under Contract",
"newStatus": "Closed",
"deal": {
"id": "8a4c2e10-9f5b-4713-b6d8-0e2a7c5f3149",
"address": "144 Peachtree Walk NE",
"city": "Atlanta",
"state": "GA",
"zip": "30309",
"county": "Fulton",
"side": "Buyer",
"type": "Purchase",
"status": "Closed",
"purchasePrice": 485000,
"closingDate": "2026-09-15",
"tcFee": 395,
"commissionAmount": 14550,
"commissionRate": 3,
"totalCommissionAmount": 29100,
"buyerAgentCommissionAmount": 14550,
"sellerAgentCommissionAmount": 14550,
"buyerAgentCommissionRate": 3,
"sellerAgentCommissionRate": 3,
"office": {
"id": "c3a8e1f4-7b20-4d59-a6e3-9f0b2d4c8e17",
"name": "Peachtree Realty Group"
},
"assignedTo": {
"id": "1c9e6b40-72af-4d35-8e21-5b0a3f7c9d16",
"name": "Jordan Rivera",
"email": "jordan@example.com"
},
"agents": [
{ "role": "listing", "name": "Sam Patel", "email": "sam@example.com", "brokerage": "Northside Homes", "phone": "404-555-0142" },
{ "role": "buyer", "name": "Alex Chen", "email": "alex@example.com", "brokerage": "Peachtree Realty Group", "phone": "404-555-0187" }
],
"customFields": {
"lockbox_code": "4417",
"hoa_transfer_fee": null
}
}
}
}
Commission rates are in percentage points (3 means 3%). commissionAmount and commissionRate are for your side of the deal; on a dual-agency deal they add both sides together. customFields holds only your active custom fields, keyed by merge code; each value is the stored text, or null when unset.
A deal that moves into a closed status sends transaction.status_changed and transaction.closed; both carry the same deal snapshot. For a plain-language walkthrough of sending these to a CRM, with a table of every deal field, see Send deal data to GoHighLevel or another CRM.
Each request also carries these headers:
| Header | Value |
|---|---|
X-Webhook-Signature | sha256=<hmac-hex> — see below |
X-Webhook-Event-Type | e.g. extraction.completed |
X-Webhook-Event-Id | Unique per delivery attempt |
X-Webhook-Timestamp | ISO 8601 send time |
Verify the signature
DocJacket signs every payload with HMAC-SHA256 using your subscription's secret. Always verify before trusting a delivery — compute the HMAC of the raw request body and compare it to the X-Webhook-Signature header.
// Node.js (Express) — use the RAW body, not the parsed JSON
import crypto from 'crypto';
function verify(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(signatureHeader);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post('/hooks/docjacket',
express.raw({ type: 'application/json' }),
(req, res) => {
if (!verify(req.body, req.get('X-Webhook-Signature'), process.env.DJ_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
// …handle event.eventType / event.data…
res.status(200).end();
});
# Python (Flask)
import hmac, hashlib
def verify(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, signature_header or "")
Return a 2xx quickly (do heavy work asynchronously). Any non-2xx — or a timeout — counts as a failed delivery.
Retries, circuit breaker & delivery history
- Retries — failed deliveries retry with backoff.
- Circuit breaker — if an endpoint fails repeatedly, DocJacket automatically pauses it to avoid flooding a down service. Re-enable it (and reset the breaker) once you've fixed the endpoint.
- Delivery history —
GET /api/v1/webhooks/{id}/deliveries(or the Connections page) shows recent attempts with status codes, latency, errors, and the next retry time, so you can self-debug.
Test it
Fire a test delivery to confirm your endpoint and signature check work before going live:
curl -X POST https://api.docjacket.com/api/v1/webhooks/SUBSCRIPTION_ID/test \
-H "Authorization: Bearer mcp_at_YOUR_KEY"