Docs
Integrations

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

  1. 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"]
  }'
  1. Save the secret from the response -- it is shown only once and is needed to verify delivery signatures.

  2. Implement a handler at your URL that verifies signatures and returns a 2xx response 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.

  1. GridWonk

    Sign the raw JSON body

    The sender computes HMAC-SHA256 over the timestamp, a period, and the exact serialized request body.

  2. Receiver

    Read the body once

    Capture the raw request string a single time; do not parse or consume the request stream first.

  3. Receiver

    Validate timestamp and signature

    Reject stale timestamps, compare equal-length signature buffers in constant time, and stop on failure.

  4. Receiver

    Deduplicate and enqueue

    Use the event ID for idempotency, parse only after verification, and enqueue slow work outside the response path.

  5. 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

EventDescriptionCurrently Emitted
job.completedAsync job finished successfullyYes
job.failedAsync job failedYes
document.digitizedDocument digitization completedYes
requirements.extractedHistorical event from the retired legacy requirements read; no longer emitted because that endpoint is 410No
document.classifiedDocument classification completedReserved
document.validatedValidation workflow produced resultVia job.completed
deficiency.foundDeficiency identifiedReserved
screening.completeScreening workflow completedReserved
utility.screening.determinedHuman screening determination savedImplemented; dependent release held
utility.finding.adjudicatedFinding decision savedImplemented; dependent release held
utility.study_requirement.setCase-specific study requirement changedImplemented; dependent release held
utility.package.frozenNew immutable package createdImplemented; dependent release held
utility.notice.status_changedCanonical notice lifecycle changedImplemented; 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:

FamilyData
ScreeningPackage/run IDs and screened to complete
FindingPackage/run/adjudication IDs, adjudication version, finding ID and decision
StudyEvidence ID/version and normalized case-specific requirement
PackagePackage ID/number/type and pinned run ID when present
NoticeNotice 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:

HeaderExamplePurpose
Content-Typeapplication/jsonPayload format
X-GridWonk-Eventjob.completedEvent type
X-GridWonk-Event-Ida1b2c3d4:job.completedUnique event ID (for deduplication)
X-GridWonk-Timestamp1740009600Unix epoch seconds
X-GridWonk-Signaturesha256=abc123...HMAC-SHA256 signature

Removed aliases: deliveries previously also carried X-GridBoost-Event, X-GridBoost-Event-Id, X-GridBoost-Timestamp, and X-GridBoost-Signature with identical values. Those aliases have been removed — use the X-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:

  1. Verify the signature before any processing.
  2. Return 200 OK immediately -- perform heavy work asynchronously.
  3. Deduplicate using the X-GridWonk-Event-Id header 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.

AttemptDelayCumulative Wait
1Immediate0
230 seconds30s
32 minutes2m 30s
410 minutes12m 30s
51 hour1h 12m 30s
66 hours7h 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, consecutiveFailures resets to 0

Circuit Breaker

This subscription-wide breaker applies to legacy delivery only.

After 10 consecutive failures, the subscription is automatically disabled:

  • isActive set to false
  • disabledAt timestamp 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:

PathLifecycleRetriesLoggingUse Case
Legacy subscription-basedPersistentYes (6 attempts)Full delivery logJob/document monitoring
Utility-event subscriptionPer application/subscriptionUp to 8; stream-local holdImmutable event, logical delivery and attempt logOrdered utility changes; dependent release held
Direct webhookPer-jobNormally one attempt; stale-job recovery permits up to fiveRecovery outcome summary for stale jobsJob 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

  1. Always verify signatures -- never process unverified payloads.
  2. Respond quickly -- return 2xx within 10 seconds. Process payloads asynchronously.
  3. Handle duplicates -- use X-GridWonk-Event-Id for idempotency. The same event may be delivered more than once.
  4. Monitor subscription health -- check consecutiveFailures via GET /api/v1/webhooks and re-enable disabled subscriptions after fixing your endpoint.
  5. Reject stale timestamps -- discard deliveries older than 5 minutes to prevent replay attacks.
  6. Use HTTPS only -- webhook URLs must use HTTPS. HTTP endpoints are rejected at subscription time.
  7. Log delivery IDs -- store event IDs for debugging and auditing failed deliveries.

Next Steps

On this page