Integration guides

Webhooks

Receive events instead of polling. Every delivery is signed.

Set a callback_url on a request, or a workspace-level URL in the Synquery app. Each delivery is a POST with a JSON body. For request events, data is the same body Get status and answers returns at the time of the event. Billing events carry no request_id.

Verifying signatures

Each delivery has a Synquery-Signature header in the form t=<unix seconds>,v1=<hex>. v1 is an HMAC SHA-256 of "<t>.<raw body>", using your webhook secret, which is shown once when you create the endpoint. Reject the delivery if the signature doesn't match or t is more than 5 minutes old.

Delivery rules

  • Respond with a 2xx status within 10 seconds. Otherwise Synquery retries with backoff for 24 hours.
  • Deliveries may repeat or arrive out of order. De-duplicate on id.
  • For the current state of a request, fetch GET /questions/{request_id} rather than assuming the latest delivery is the latest state.

Events

The request was accepted and sourcing has started.
One per confirmed expert answer; data holds that response.
The request completed, fully or partially.
The request failed, with a reason.
A follow-up round completed.
Credits fell below the threshold you set.
A spend or volume limit stopped a request.

Delivery body

idstring#
Event ID. De-duplicate on it.
eventstring#
One of the events above.
When the event was created.
The request, for request events.
dataobject#
The request body at the time of the event, or billing details.

Next

Errors and retries →

request.completed (abbreviated)json
{
  "id": "evt_9x2k",
  "event": "request.completed",
  "created_at": "2026-10-28T09:42:00Z",
  "request_id": "q_8f2k1x",
  "data": {
    "request_id": "q_8f2k1x",
    "status": "complete",
    "partial": false,
    "experts_delivered": 5
  }
}
billing.low_balancejson
{
  "id": "evt_3m8q",
  "event": "billing.low_balance",
  "created_at": "2026-10-28T09:45:00Z",
  "data": {"workspace_id": "ws_1a2b", "credits_remaining": 40, "threshold": 50}
}
Verify a signature (Node.js)javascript
import crypto from "node:crypto"

// header: the Synquery-Signature value; rawBody: the unparsed request body
function verifySynquerySignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")))
  const ageSeconds = Math.floor(Date.now() / 1000) - Number(parts.t)
  if (!parts.t || !parts.v1 || ageSeconds > 300) return false
  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex")
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}