Skip to content

Partner API Webhooks

Webhooks push Partner API events to your server so you don't have to poll: an import finishes, a patient submits, a completed PDF is ready. Deliveries are signed and PHI-minimized by design — events carry ids and retrieve URLs, never patient answers.

Registering

POST /webhooks (scope webhooks:manage) registers a delivery URL (public HTTPS only) and mints its signing secret:

sh
curl -X POST -H "Authorization: Bearer $EDF_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/edf-webhook","events":["submission.created"]}' \
  https://form.easydocforms.com/api/v1/webhooks

The whsec_* secret is returned exactly once, in this response — store it. Omitting events (or sending an empty array) subscribes to all events. At most 10 active webhooks per organization. GET /webhooks lists active subscriptions (without secrets); DELETE /webhooks/{webhook_id} deactivates one.

POST /webhooks/{webhook_id}/test synchronously delivers one signed test event so you can verify your receiver and signature check end to end — the response reports delivered, your endpoint's status_code, or the transport error.

Events

EventFires whenData
import.completedA queued import finished and its template is readyimport_id, template_id, page_count, field_count, review_required
import.failedA queued import failed (invalid PDF, processing error)import_id, error
submission.createdA patient submitted a form via one of your fill linkssubmission_id, template_id, fill_link_id, external_ref, retrieve_url
submission.pdf_readyThe completed PDF was frozen at submission and is readysubmission_id, template_id, external_ref, pdf_url
testYou called the test endpointwebhook_id

submission.created carries no answers — fetch them from retrieve_url with your API key. submission.pdf_ready fires alongside submission.created when the freeze-on-submit render succeeded (the common case); when it doesn't fire, the PDF is still retrievable — the /pdf endpoint renders on demand.

The envelope

Every delivery is an HTTP POST of:

json
{
  "event": "submission.created",
  "timestamp": "2026-08-22T11:00:05Z",
  "org_id": "org_...",
  "data": { "submission_id": "sub_...", "external_ref": "visit-8675309", "...": "..." }
}

Verifying signatures

Every delivery carries:

X-EDF-Signature: t=<unix seconds>,v1=<hex hmac-sha256(secret, "<t>.<raw request body>")>

Verify by recomputing the HMAC over the raw body bytes (before any JSON parsing), comparing v1 in constant time, and rejecting timestamps more than 5 minutes from now (the replay window). Signatures are minted fresh per delivery attempt.

js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((kv) => kv.split("=", 2))
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.t || !parts.v1 || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(parts.v1, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

All four SDKs ship a webhook signature verifier — use it instead of hand-rolling one.

Retries and dedup

  • Every delivery carries X-EDF-Delivery-Id, minted once per event and reused verbatim on retries — deduplicate on it.
  • Failed deliveries (network errors, 408/429/5xx) are retried with exponential backoff: roughly 1 minute doubling toward a 6-hour cap, up to 10 attempts over about a day. Each retry is re-signed fresh, so the timestamp check keeps working.
  • A non-429 4xx response stops retries permanently.
  • Return 2xx fast (do the work after acknowledging). If you were down longer than the retry window, reconcile with GET /submissions?submitted_since=....

HIPAA-aware documentation for independent healthcare practices.