Webhooks
An Admin adds webhooks on API clients and webhooks: an HTTPS URL on a host of the firm's webhook allow-list, and the events to send. Vestoris refuses URLs that resolve to private, loopback or reserved addresses (checked again at every delivery, with the connection pinned to the checked address) and does not follow redirects.
| Event | When |
|---|---|
order.created | An order was created (blotter, pipeline, rebalance or the API) |
order.updated | An order changed: amended, approved, rejected, released, cancelled, executed or reworked |
fill.created | A simulated fill was recorded |
violation.created | A compliance violation was raised (pre-trade, in-trade or post-trade) |
violation.updated | A violation was acknowledged, resolved, waived or escalated |
workflow.item.updated | A workflow item was approved in part, escalated by hand, re-prioritized or moved |
workflow.sla_breached | A workflow item passed its SLA and was escalated automatically |
feed.run.updated | A feed file arrived, was applied, held, rejected, failed, or is late or missed |
sod.status | The start-of-day chain changed state (running, blocked, complete, overridden) |
rule.activated | A compliance or workflow rule was signed off and activated |
rule.retired | A compliance or workflow rule was retired |
ping | A test event sent from the webhook page or POST /webhooks/{id}/ping |
The delivery
POST /your/endpoint HTTP/1.1
Content-Type: application/json
Vestoris-Event: order.updated
Vestoris-Delivery: 1234
Vestoris-Event-Id: evt_6f1c...
Vestoris-Signature: t=1792980000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{"id":"evt_6f1c...","type":"order.updated","created_at":"2026-10-26T14:05:00Z","firm_id":"3","paper_trading":true,"data":{...}}Answer with any 2xx within 5 seconds. Otherwise Vestoris retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then marks the delivery dead. Every attempt is in the delivery log; any delivery can be sent again. Deliveries can arrive more than once or out of order: use id to deduplicate.
Verifying the signature
The signature is HMAC-SHA256 with your webhook's signing secret (whsec_..., shown once) over "<t>.<raw body>". Check it with a constant-time comparison and refuse a t more than 300 seconds from now (the replay window).
PHP:
function vestoris_verified(string $secret, string $header, string $body, int $window = 300): bool {
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)) return false;
if (abs(time() - (int) $m[1]) > $window) return false;
return hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
}
$ok = vestoris_verified(getenv('VESTORIS_WEBHOOK_SECRET'), $_SERVER['HTTP_VESTORIS_SIGNATURE'] ?? '', file_get_contents('php://input'));Python:
import hmac, hashlib, re, time
def vestoris_verified(secret: str, header: str, body: bytes, window: int = 300) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m.group(1))) > window:
return False
mac = hmac.new(secret.encode(), m.group(1).encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, m.group(2))Node.js:
const crypto = require('crypto');
function vestorisVerified(secret, header, rawBody, windowSec = 300) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > windowSec) return false;
const mac = crypto.createHmac('sha256', secret).update(m[1] + '.').update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(m[2]));
}Verify the raw body, before any JSON parsing. Keep the secret in your secret manager; "New secret" on the webhook replaces it at once.