Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.have-foresight.app/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks let Foresight push state changes to your systems instead of forcing you to poll. We deliver signed JSON over HTTPS, retry on failure, and surface every delivery (and every replay) in the dashboard.

Creating a subscription

Create a subscription via the dashboard at Settings → Webhooks, or via the API:
curl -X POST "$FORESIGHT_BASE_URL/webhooks/subscriptions" \
  -H "X-API-Key: $FORESIGHT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/foresight",
    "events": ["claim.submitted", "claim.paid", "claim.denied"],
    "description": "Production claim event sink"
  }'
The response includes a signingSecret (shown once). Store it in your secrets manager — you’ll use it to verify every incoming delivery.

Delivery format

Every delivery is a POST with Content-Type: application/json and the following headers:
HeaderDescription
Foresight-SignatureHMAC-SHA256 signature of the payload (see below).
Foresight-TimestampUnix timestamp when the delivery was signed.
Foresight-EventEvent type (e.g. claim.paid).
Foresight-Event-IdUnique event ID. Use for idempotency on your side.
Foresight-Delivery-IdUnique delivery ID. Different across retries of the same event.
Foresight-Subscription-IdThe subscription that fired this delivery.
User-AgentForesight-Webhooks/1.0
Body (truncated example):
{
  "id": "evt_01J5KA9XQR9V2W4Y6Z8B0D2F4H",
  "type": "claim.paid",
  "createdAt": "2026-05-03T19:42:11.402Z",
  "data": {
    "claim": {
      "id": "clm_01J5K8XQ7M3N5R8T2W6Y9Z1B4D",
      "status": "paid",
      "totalCharges": 45000,
      "amountPaid": 38250
    }
  }
}

Signature verification

We sign every delivery with HMAC-SHA256 using your subscription’s signingSecret. Always verify the signature before processing.
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhook(req) {
  const signature = req.headers['foresight-signature'];
  const timestamp = req.headers['foresight-timestamp'];
  const body = req.rawBody; // raw bytes, NOT parsed JSON

  // Reject deliveries older than 5 minutes (replay protection)
  const age = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (age > 300) throw new Error('Webhook too old');

  const expected = createHmac('sha256', process.env.FORESIGHT_WEBHOOK_SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  if (
    !timingSafeEqual(
      Buffer.from(signature, 'hex'),
      Buffer.from(expected, 'hex')
    )
  ) {
    throw new Error('Invalid signature');
  }
}
You must verify against the raw request body, not a re-serialized JSON object. Re-serialization changes whitespace and breaks the signature.

Acknowledging delivery

Return 2xx within 10 seconds. Anything else (including timeouts and 5xx) is treated as a failure and retried. If the work you do in response to a webhook is slow, return 200 immediately and process the event asynchronously on your side.

Retries

Failed deliveries retry with exponential backoff:
AttemptDelay
1Immediate
230 seconds
35 minutes
430 minutes
52 hours
612 hours
724 hours (final)
After 7 failed attempts, the delivery is marked permanently failed. The subscription stays active. If a subscription has 100 consecutive failed deliveries, it’s auto-disabled and we email the subscription owner. You can manually replay any delivery from the dashboard or via:
POST /v1/webhooks/subscriptions/{subscriptionId}/deliveries/{deliveryId}/replay

Idempotency

Use Foresight-Event-Id to dedupe on your side — under retries, the same event ID can arrive multiple times, but the delivery ID will differ.
async function handleWebhook(req) {
  const eventId = req.headers['foresight-event-id'];
  if (await db.processedEvents.has(eventId)) return; // already handled
  await db.processedEvents.add(eventId);
  // ...do the work
}

Event catalog

The full event catalog is at the webhook subscriptions reference. Common events include:
EventTriggers when
patient.createdA new patient record is created.
patient.updatedA patient record is updated.
eligibility.completedAn eligibility check finishes (sync or async).
prior_auth.submittedA PA was submitted to the payer.
prior_auth.approvedA PA was approved.
prior_auth.deniedA PA was denied.
prior_auth.questions_receivedThe payer sent additional questions.
claim.submittedA claim was submitted to the clearinghouse.
claim.acceptedA claim was accepted by the payer (277CA).
claim.rejectedA claim was rejected at the clearinghouse or front-end.
claim.deniedA claim was adjudicated and denied.
claim.paidA claim was paid (835 ERA posted).
denial.receivedA denial was received and parsed.
appeal.sentAn appeal letter was sent.
appeal.outcome_receivedAn appeal outcome was received.
Subscribe to specific event types with the events array on your subscription, or pass ["*"] to receive all events.

IP allowlist

If you allow only specific egress IPs, our webhook origin IPs are published at /v1/webhooks/origin-ips and updated at least 30 days before any change.