Examples in these docs use synthetic data, not real people. Protearc reports findings, not a diagnosis.
ProtearcDocs

Webhooks

Webhooks

A webhook is a URL on your side that Protearc calls when something happens to one of your members' results. Each organization registers its own addresses and receives only its own events.

Register an endpoint

POST /v1/webhooks with {"url": "..."} registers an address and returns its id and signing secret (whsec_...). The secret is in that response only; store it like a password.

  • The address must be a public https:// URL. It may not contain a username, a password or a fragment, and addresses that are private, loopback or link-local, or that use a local name, are refused with 422. The local demo also accepts http://localhost and http://127.0.0.1.
  • An organization can have at most 10 endpoints. An eleventh is refused with 409 (endpoint limit reached), and so is an address that is already registered (url already registered).
  • An endpoint receives events created after it was registered, not earlier ones.

GET /v1/webhooks lists your endpoints with the outcome of the last delivery (delivered or failed); it never returns a secret. DELETE /v1/webhooks/{webhook_id} removes one and answers 204.

curl -X POST http://localhost:8000/v1/webhooks \
  -H "Authorization: Bearer ptk_sandbox_meridian_demo" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/protearc/hooks"}'

Events

EventSent whendata.status
result.releasedThe laboratory releases a result.released
qc.holdThe laboratory puts a result on a quality-control hold.qc_hold

A third type, webhook.test, is sent when an administrator presses the test button on the endpoint in Arc Watch, under Settings, then Integrations. Its data is empty.

The body is JSON. It carries ids and a status, never a name or a birth date. To read the results, call the reports route with data.member_ref.

The id identifies the event and stays the same across retries. Keep the ids you have handled and ignore a repeat.

json Body of a result.released event (synthetic example)
{
  "created_at": "2026-11-04T16:31:08.512345+00:00",
  "data": {
    "draw_number": 4,
    "member_ref": "PA-0001",
    "report_id": "3b6f0c9e-52c4-4a43-8f27-7d1f0a6e9c10",
    "status": "released"
  },
  "id": "9c1d7a52-0e0b-4a4e-a4f5-2f6f9d1b7c33",
  "type": "result.released"
}

Headers

Every delivery is a POST with these headers.

HeaderValue
Content-Typeapplication/json
X-Protearc-EventThe event type, for example result.released.
X-Protearc-Attempt1 for the first try, up to 4.
X-Protearc-TimestampUnix time in seconds when this attempt was signed. It is new on each attempt.
X-Protearc-Signaturesha256= followed by the hex digest described below.

The signature

The signature is an HMAC-SHA256, keyed with your endpoint's secret, over the timestamp, a period and the raw request body, exactly as received:

Write it as sha256= plus the lowercase hex digest, and compare it with the header in constant time. Use the raw bytes of the body: parsing the JSON and writing it back out can change the bytes and break the match. Reject a request whose signature does not match.

Protearc does not check the age of a timestamp for you. To limit replays, also reject a request whose timestamp is more than a few minutes from your clock.

In a Node web framework, read the body as a Buffer (for example express.raw({ type: "application/json" })) before anything parses it.

text
HMAC_SHA256(secret, timestamp + "." + raw_body)
Verify a signature, Python
import hashlib
import hmac


def verify(secret, timestamp, body, signature):
    """secret: str, timestamp: str (the header), body: bytes (raw), signature: str (the header)."""
    signed = timestamp.encode() + b"." + body
    expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), signature.encode())
Verify a signature, Node
const crypto = require("node:crypto");

// secret: string, timestamp: string (the header), body: Buffer (raw), signature: string (the header)
function verify(secret, timestamp, body, signature) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret)
    .update(`${timestamp}.`).update(body).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Retries and the delivery log

Answer with any 2xx status to acknowledge. Anything else, or no answer at all, counts as a failed attempt.

  • A failed attempt is retried up to three times, after waits of 1, 2 and 4 seconds, so an event is tried at most four times. Each attempt is signed afresh with a new timestamp.
  • Every attempt is logged with its number, the status code you returned or the error if none was received. The outcome for the event, delivered or failed, is recorded per endpoint.
  • An event that failed all four attempts is not retried on its own. In Arc Watch, Settings, Integrations shows each address as healthy or failing with the number of undelivered events; Retry now queues them again.
  • When an address starts failing, the administrators of your organization get one email. It names no member and no address, only that a webhook address is failing.
  • Treat delivery as at least once and deduplicate on the event id.

Rotate the secret

POST /v1/webhooks/{webhook_id}/rotate-secret returns a new secret, shown in that response only. The old secret stops signing at once: every delivery after the call is signed with the new one, so update your receiver with it straight away. An unknown webhook_id is 404.

curl -X POST http://localhost:8000/v1/webhooks/5d0c8a1e-6b1f-4f6e-9b0e-0c9a4d7f2b11/rotate-secret \
  -H "Authorization: Bearer ptk_sandbox_meridian_demo"