Webhook Integration Guide
Event types, payload schemas, retry semantics, signature verification, and integration patterns for GridWonk webhooks.
Webhook Integration Guide
GridWonk webhooks deliver real-time HTTP POST notifications to your HTTPS endpoints when async jobs, document operations, and compliance events occur.
Utility manual freeze, adjudication, screening completion, and study-requirement bindings record
their existing audit/history provenance. The dependent utility-event layer now implements five
utility.* families for those saved changes and canonical notice transitions, with durable,
ordered delivery through receiving-utility subscriptions. Schema activation and production release
remain held. A subscription grants no decision, notice-send, or provider-receipt authority.
For the complete CRUD API reference (create, list, update, delete subscriptions), see Webhooks API.
The SDK and CLI default to the canonical https://gridwonk.com API origin, so authenticated
subscription management does not need a base URL or trust override.
Quick Setup
- Create a subscription via the Webhooks API:
curl -X POST https://gridwonk.com/api/v1/webhooks \
-H "Authorization: Bearer $GRIDBOOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/hooks/gridwonk",
"events": ["job.completed", "job.failed", "document.digitized"]
}'-
Save the
secretfrom the response -- it is shown only once and is needed to verify delivery signatures. -
Implement a handler at your URL that verifies signatures and returns a
2xxresponse within 10 seconds.
Signed webhook delivery sequence
Verify the exact raw body before parsing it, acknowledge quickly, and make downstream work idempotent because subscription delivery is at least once.
GridWonk
Sign the raw JSON body
The sender computes HMAC-SHA256 over the timestamp, a period, and the exact serialized request body.
Receiver
Read the body once
Capture the raw request string a single time; do not parse or consume the request stream first.
Receiver
Validate timestamp and signature
Reject stale timestamps, compare equal-length signature buffers in constant time, and stop on failure.
Receiver
Deduplicate and enqueue
Use the event ID for idempotency, parse only after verification, and enqueue slow work outside the response path.
Delivery service
Acknowledge or retry
A 2xx response succeeds. Subscription failures retry with backoff. Direct callbacks normally have one attempt; callbacks recovered by stale-job cleanup have bounded retries.
Source anchors: lib/api/v1/webhooks.ts, app/api/v1/webhooks/route.ts
Before each POST, GridWonk revalidates the destination through the public-network URL guard. Delivery does not follow redirects, and it reads at most the first 2,000 response characters for status and diagnostics.
Event Types
| Event | Description | Currently Emitted |
|---|---|---|
job.completed | Async job finished successfully | Yes |
job.failed | Async job failed | Yes |
document.digitized | Document digitization completed | Yes |
requirements.extracted | Historical event from the retired legacy requirements read; no longer emitted because that endpoint is 410 | No |
document.classified | Document classification completed | Reserved |
document.validated | Validation workflow produced result | Via job.completed |
deficiency.found | Deficiency identified | Reserved |
screening.complete | Screening workflow completed | Reserved |
utility.screening.determined | Human screening determination saved | Implemented; dependent release held |
utility.finding.adjudicated | Finding decision saved | Implemented; dependent release held |
utility.study_requirement.set | Case-specific study requirement changed | Implemented; dependent release held |
utility.package.frozen | New immutable package created | Implemented; dependent release held |
utility.notice.status_changed | Canonical notice lifecycle changed | Implemented; dependent release held |
Reserved events are defined in the schema but not yet emitted. Subscribe to them now for forward compatibility.
The default-disabled Salesforce connector does not gain
activation or write-back authority from a subscription. Its existing authorized freeze transaction
records utility.package.frozen with exact persisted import/job provenance when the utility-event
layer is released. Its private result delivery remains separate. Subscribing to either a reserved
screening event or a utility event does not enable the connector.
Payload Schemas
Utility events
The utility-notice stack implements the five utility families together; schema activation and production release remain separate human approval gates. Events are committed alongside the business change, with no historical backfill. Receiving-utility subscriptions receive them; applicant/consultant membership or delegated application access does not widen subscription scope.
{
"eventId": "c6dd50a1-9cd8-4dbd-95b3-bbf1479985de",
"eventType": "utility.notice.status_changed",
"payloadVersion": 1,
"organizationId": "95da3428-9e60-47d3-852c-690567423009",
"submissionId": "8996aa1c-f3c5-4f73-b477-309738428244",
"sequence": "7",
"occurredAt": "2026-10-10T12:00:00.000Z",
"provenance": {
"userId": "c2d4e8eb-45d0-4e1e-9a1e-dbd07f069bb1",
"credential": "session",
"surface": "console",
"requestId": "example-request"
},
"data": {
"noticeId": "9d05ff96-9657-4bb8-9eb3-c35cc233ae46",
"contentRevision": 2,
"packageId": null,
"packetReviewRunId": null,
"fromStatus": "approved",
"toStatus": "sent"
}
}Keep sequence as a decimal string or arbitrary-precision integer, never a JavaScript number.
data varies by event:
| Family | Data |
|---|---|
| Screening | Package/run IDs and screened to complete |
| Finding | Package/run/adjudication IDs, adjudication version, finding ID and decision |
| Study | Evidence ID/version and normalized case-specific requirement |
| Package | Package ID/number/type and pinned run ID when present |
| Notice | Notice ID/content revision, optional package/run IDs, previous/new canonical status |
Package events cover existing manual, automatic/recovery, Salesforce and applicant submit/resubmit
freezes. Human screening events do not mean automated job completion; existing job.completed
is unchanged. Replays, unchanged study requirements, content-only edits, send claims and provider
receipts do not emit another canonical event. A failed event/fan-out insert rolls back the business
change. Notice events omit recipient addresses and complete email bodies.
Each subscription/application pair has its own ordered stream. A consumer may see sequence 1 followed by 3 when it did not subscribe to event 2. Stable IDs allow deduplication after a crash or lost response. Retries reuse the stored payload and ID, refreshing only signing time/signature. Persist your deduplication decision and accepted work before returning success.
job.completed / job.failed
{
"jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"jobType": "discovery",
"status": "complete",
"error": null,
"result": {
"success": true,
"response": "Found 12 interconnection requirements for Duke Energy...",
"structuredOutput": { },
"tokenUsage": { "input": 5000, "output": 1200 },
"executionTime": 12345,
"error": null
}
}Creatable job types: discovery, document-digitization. Historical validation, location, and report job types remain readable but return 410 JOB_TYPE_RETIRED when creation is attempted. Applicant-document jobs require an organization-visible document that the API key is allowed to process. Generic jobs reject Salesforce-owned documents; a webhook subscription does not grant processing permission.
For the upload that creates a document-digitization job, compatible whitespace-wrapped and URL-safe base64 is normalized before the PDF signature and decoded-size checks. The stored size comes from the decoded bytes rather than the caller-provided count.
document.digitized
{
"documentId": "550e8400-e29b-41d4-a716-446655440000",
"tableName": "ix_applicant_documents",
"alreadyDigitized": false,
"metadata": {
"inputTokens": 2400,
"outputTokens": 800,
"durationMs": 1200
}
}requirements.extracted
{
"utility": "duke-energy",
"region": "NC",
"category": "site_control",
"count": 15
}Historically, only GET /api/v1/requirements emitted this event. That route is now retired. The
customer-release replacement
GET /api/v1/published-requirements has no webhook side effect at all — a read is not an
extraction. The legacy endpoint is now a permanent 410 tombstone, so requirements.extracted is
no longer emitted.
This event ends with the legacy endpoint
requirements.extracted has no other producer. The legacy endpoint now returns 410 Gone, so
the event has stopped permanently — an existing subscription stays valid but remains quiet.
The shutdown is complete. The Sunset was 2026-08-11T19:00:00Z, and authenticated calls now
receive typed 410 Gone with the published endpoint as the replacement. See
Retirement Stages.
If this event drives downstream work, migrate that trigger to your own scheduled read of
GET /api/v1/published-requirements rather than waiting for the last delivery. That endpoint
returns every published requirement except the ones a utility reviewer explicitly rejected, with
each row marked reviewStatus: "approved" or reviewStatus: "pending".
count follows the same requirement lookup mode as the legacy API response that emitted the event. In shadow mode, it can reflect the existing displayable corpus; in enforcement mode, it reflects only customer-usable requirements linked to current usable procedure documents.
Receiving Webhooks
Delivery Headers
Every webhook delivery includes these headers:
| Header | Example | Purpose |
|---|---|---|
Content-Type | application/json | Payload format |
X-GridWonk-Event | job.completed | Event type |
X-GridWonk-Event-Id | a1b2c3d4:job.completed | Unique event ID (for deduplication) |
X-GridWonk-Timestamp | 1740009600 | Unix epoch seconds |
X-GridWonk-Signature | sha256=abc123... | HMAC-SHA256 signature |
Removed aliases: deliveries previously also carried
X-GridBoost-Event,X-GridBoost-Event-Id,X-GridBoost-Timestamp, andX-GridBoost-Signaturewith identical values. Those aliases have been removed — use theX-GridWonk-*headers above.
Signature Verification
Subscription deliveries are signed using your subscription secret. Direct one-shot webhooks are signed with the platform direct-webhook signing secret when configured. Always verify signatures before processing payloads.
Signing Algorithm
HMAC-SHA256(secret, "{timestamp}.{raw-json-body}")The signature header contains sha256= followed by the hex-encoded HMAC digest.
Node.js
The verifier below is a runnable utility when passed the exact raw request body:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhook(
headers: Headers,
rawBody: string,
secret: string
): boolean {
const signature = headers.get('X-GridWonk-Signature') ?? '';
const timestamp = headers.get('X-GridWonk-Timestamp') ?? '';
// Reject stale deliveries (> 5 minutes old)
const sentAt = Number(timestamp);
const age = Math.abs(Date.now() / 1000 - sentAt);
if (!Number.isFinite(sentAt) || age > 300) return false;
const expected = `sha256=${
createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
}`;
const actualBytes = Buffer.from(signature);
const expectedBytes = Buffer.from(expected);
if (actualBytes.length !== expectedBytes.length) return false;
return timingSafeEqual(actualBytes, expectedBytes);
}Python
import hmac, hashlib, time
def verify_webhook(signature: str, timestamp: str, body: bytes, secret: str) -> bool:
# Reject stale deliveries
if abs(time.time() - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(
secret.encode(),
f"{timestamp}.{body.decode()}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Handling Deliveries
A minimal webhook handler should:
- Verify the signature before any processing.
- Return
200 OKimmediately -- perform heavy work asynchronously. - Deduplicate using the
X-GridWonk-Event-Idheader to handle at-least-once delivery.
The following handler is illustrative because enqueueEvent is application-specific. It reads the request body exactly once, verifies those exact bytes, then parses the JSON:
export async function POST(req: Request) {
const secret = process.env.GRIDBOOST_WEBHOOK_SECRET!;
const rawBody = await req.text();
if (!verifyWebhook(req.headers, rawBody, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const event = req.headers.get('X-GridWonk-Event');
const eventId = req.headers.get('X-GridWonk-Event-Id');
const payload = JSON.parse(rawBody);
// Enqueue for async processing (don't block the response)
await enqueueEvent({ event, eventId, payload });
return new Response('OK', { status: 200 });
}Retry Semantics
Subscription deliveries use an at-least-once model with exponential backoff.
Retry Schedule
This six-attempt schedule applies to legacy job/document subscription delivery, not utility events.
| Attempt | Delay | Cumulative Wait |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2m 30s |
| 4 | 10 minutes | 12m 30s |
| 5 | 1 hour | 1h 12m 30s |
| 6 | 6 hours | 7h 12m 30s |
Total: 6 attempts (1 initial + 5 retries). Max wait before final attempt is approximately 7 hours.
What Counts as Failure
- HTTP response outside 200-299 range
- Network error or timeout (10-second delivery timeout)
- DNS resolution failure
- URL resolves to a private or reserved IP address when the destination is revalidated for the delivery attempt
What Counts as Success
- HTTP 2xx response (any status in the 200-299 range)
- On success,
consecutiveFailuresresets to 0
Circuit Breaker
This subscription-wide breaker applies to legacy delivery only.
After 10 consecutive failures, the subscription is automatically disabled:
isActiveset tofalsedisabledAttimestamp recorded- No further deliveries attempted
To re-enable a disabled subscription, send PATCH /api/v1/webhooks/{id} with {"isActive": true}. This resets the failure counter. See the Webhooks API for details.
Utility-event retry and recovery
Utility events have at most eight attempts. Minimum retry delays are 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 12 hours and 24 hours. An authenticated five-minute recovery job selects at most 50 eligible stream heads before applying its limit and publishes with concurrency five. A failed publication leaves the durable row due. A three-minute claim prevents duplicate workers from independently posting while the ten-second HTTPS request is active.
An exhausted event holds its subscription/application pair and later events in that pair. Other applications and subscribers can continue. Utility attempts never alter subscription-wide failure counters or auto-disable it. A late worker can record its own attempt result but cannot regress a newer claim. Signature verification, HTTPS, private-address/DNS-rebinding checks, redirect refusal and bounded response reads apply to both delivery policies.
Subscription event-list changes affect future fan-out only. Disable pauses new claims, though an in-flight request may finish; re-enable resumes eligible pending rows, not exhausted holds. Deletion removes that subscription's deliveries but retains source events. Do not reset held rows, attempt counts or event IDs to manufacture a retry; operator disposition requires separate review.
Delivery Paths
Packet-review cancellation emits its existing failed-job terminal notification only after the guarded job/review settlement succeeds. Notification failure cannot undo that settlement, and cancellation introduces no new event name or signing scheme. A terminal notification describes the saved job outcome, not proof that an already-running external provider request stopped.
GridWonk has the following delivery mechanisms:
| Path | Lifecycle | Retries | Logging | Use Case |
|---|---|---|---|---|
| Legacy subscription-based | Persistent | Yes (6 attempts) | Full delivery log | Job/document monitoring |
| Utility-event subscription | Per application/subscription | Up to 8; stream-local hold | Immutable event, logical delivery and attempt log | Ordered utility changes; dependent release held |
| Direct webhook | Per-job | Normally one attempt; stale-job recovery permits up to five | Recovery outcome summary for stale jobs | Job completion callback |
Direct webhooks are specified via the webhookUrl parameter on supported job creation endpoints (POST /api/v1/jobs and POST /api/v1/agents/execute) and require a configured direct-webhook signing secret. The normal direct path has no subscription-style delivery log or retry schedule.
When stale-job cleanup repairs a workflow-owned job, its terminal notification is delivered asynchronously. This recovery path permits up to five direct attempts for network/DNS failures, HTTP 408/425/429 and 5xx responses. Other HTTP failures or unsafe destinations stop retries; missing signing configuration and disabled outbound delivery are recorded separately, not as success. Internal completion hooks still run after direct retries are exhausted. Recovery keeps the existing event IDs and payload contract. Always deduplicate: a successful POST followed by a sender crash can produce a repeated event.
Best Practices
- Always verify signatures -- never process unverified payloads.
- Respond quickly -- return 2xx within 10 seconds. Process payloads asynchronously.
- Handle duplicates -- use
X-GridWonk-Event-Idfor idempotency. The same event may be delivered more than once. - Monitor subscription health -- check
consecutiveFailuresviaGET /api/v1/webhooksand re-enable disabled subscriptions after fixing your endpoint. - Reject stale timestamps -- discard deliveries older than 5 minutes to prevent replay attacks.
- Use HTTPS only -- webhook URLs must use HTTPS. HTTP endpoints are rejected at subscription time.
- Log delivery IDs -- store event IDs for debugging and auditing failed deliveries.
Next Steps
- Webhooks API Reference -- full CRUD endpoint documentation
- API Quickstart -- end-to-end integration walkthrough
- Authentication & Rate Limiting -- API key management