Skip to content

Webhooks

Register an HTTPS endpoint and Asqary POSTs events to it. Keys need the webhooks:read scope to list endpoints and webhooks:manage to change them. Endpoints belong to the key’s environment (live or test), with at most 10 per environment.

Event When
backtest.completed A backtest finished. data has its ID, status, policy, dataset counts and summary.
backtest.failed A backtest could not be completed. data.error says why.
policy.activated A policy version became active. data has policy_id, name, product, version, version_id, was_tested and replaced.
policy.archived A policy was archived. data has policy_id, name and product.
webhook.test Sent only by POST /v1/webhooks/{id}/test.

Subscribe with "events": ["*"] to receive every type, including types added later. Policy events have "environment": null and go to endpoints in both environments, because policies are shared.

POST /v1/webhooks
{ "url": "https://lender.example.com/asqary/webhooks", "events": ["backtest.completed", "backtest.failed"] }

The 201 response includes "secret": "whsec_…". Store it now. It is not shown again.

POST /v1/webhooks/{id}/rotate-secret issues a new secret. For 24 hours afterwards, deliveries carry a signature from each secret.

{ "id": "evt_…", "type": "backtest.completed", "created_at": "2026-09-28T10:15:02.114Z",
"environment": "test", "data": { "backtest_id": "bt_…", "status": "completed" } }
Header Value
Asqary-Signature t=<unix seconds>,v1=<hex signature> (one v1 per valid secret)
Asqary-Event-Id The event id. The same event can arrive more than once, so deduplicate on it.
Asqary-Event-Type The event type
Asqary-Delivery-Id whd_…, as shown in the delivery log
  1. Split the header on ,. Take t and every v1.
  2. Compute HMAC-SHA256(key = the whole secret string including "whsec_", message = t + "." + raw body) as lowercase hex. Use the raw body bytes exactly as received, not re-serialised JSON.
  3. Accept if any v1 equals it (constant-time comparison) and t is within 5 minutes of your clock, to reject replays.

Test vector: secret whsec_test_secret_for_documentation_only, t=1767225600, body {"id":"evt_1","type":"webhook.test"} gives v1=3540053d3d794c1c14168b00b502d9ac4a38f4760b920f9d309d316e8efe6293.

verifyWebhook needs the raw request body:

import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@twalle/asqary";
const app = express();
app.post("/asqary/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.header("Asqary-Signature"), process.env.ASQARY_WEBHOOK_SECRET!);
} catch (err) {
if (err instanceof WebhookVerificationError) return res.sendStatus(400);
throw err;
}
res.sendStatus(200); // respond within 10 seconds; do slow work afterwards
if (event.type === "backtest.completed") {
const backtest = await asqary.backtests.get(event.data.backtestId);
}
});

While rotating secrets, pass both: verifyWebhook(body, header, [newSecret, oldSecret]). Signatures older than 5 minutes are rejected; change that with { tolerance: seconds }.

  • Respond with any 2xx within 10 seconds, and do slow work afterwards. Anything else, including a redirect, counts as a failure.
  • Failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, then every 10 hours, for up to 3 days.
  • Events can arrive out of order and more than once.
  • An endpoint that has failed for 3 days in a row is disabled and its pending deliveries fail. Re-enable it with PATCH {"status": "enabled"} and resend what you missed.
  • Asqary only delivers to public addresses. URLs must use HTTPS and must not resolve to private, loopback, link-local or other reserved addresses. This is checked again on every connection.

GET /v1/webhooks, GET|PATCH|DELETE /v1/webhooks/{id}, POST /v1/webhooks/{id}/test, GET /v1/webhooks/{id}/deliveries, GET /v1/webhooks/{id}/deliveries/{delivery_id} (payload and attempts) and POST /v1/webhooks/{id}/deliveries/{delivery_id}/resend. See the API reference.