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.
Every delivery is a POST with Content-Type: application/json and the
following headers:
| Header | Description |
|---|
Foresight-Signature | HMAC-SHA256 signature of the payload (see below). |
Foresight-Timestamp | Unix timestamp when the delivery was signed. |
Foresight-Event | Event type (e.g. claim.paid). |
Foresight-Event-Id | Unique event ID. Use for idempotency on your side. |
Foresight-Delivery-Id | Unique delivery ID. Different across retries of the same event. |
Foresight-Subscription-Id | The subscription that fired this delivery. |
User-Agent | Foresight-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:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
| 7 | 24 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:
| Event | Triggers when |
|---|
patient.created | A new patient record is created. |
patient.updated | A patient record is updated. |
eligibility.completed | An eligibility check finishes (sync or async). |
prior_auth.submitted | A PA was submitted to the payer. |
prior_auth.approved | A PA was approved. |
prior_auth.denied | A PA was denied. |
prior_auth.questions_received | The payer sent additional questions. |
claim.submitted | A claim was submitted to the clearinghouse. |
claim.accepted | A claim was accepted by the payer (277CA). |
claim.rejected | A claim was rejected at the clearinghouse or front-end. |
claim.denied | A claim was adjudicated and denied. |
claim.paid | A claim was paid (835 ERA posted). |
denial.received | A denial was received and parsed. |
appeal.sent | An appeal letter was sent. |
appeal.outcome_received | An 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.