Webhooks
Rather than polling the API, get notified.
The four events
| Event | When it fires |
|---|---|
model.trained | a version has just been trained and serves predictions |
model.stale | enough outcomes have piled up, an integration would help |
model.refreshed | an integration produced a new version |
credits.low | your credit balance is running down |
Registering an address
curl -X POST "$SPAVIK_BASE_URL/v1/webhooks" \
-H "X-API-Key: $SPAVIK_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com/webhooks/spavik","events":["model.stale","credits.low"]}'The secret appears once
The response to this creation contains a secret field. That is the only time it is shown. Put it in your secret manager immediately: there is no way to read it back.
The same address can only be registered once. A second attempt gets 409 WEBHOOK_URL_EXISTS, naming the webhook already holding the spot.
Verifying the signature
Every call is signed. Verify it before doing anything with the content: without that check, anyone who knows your URL can tell you anything.
from spavik.webhooks import verifier, SpavikSignatureError
@app.post("/webhooks/spavik")
async def receive(request):
try:
event = verifier(await request.body(), request.headers, SECRET)
except SpavikSignatureError:
return Response(status_code=400)
handle(event)
return Response(status_code=200)app.post('/webhooks/spavik', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = await verifier(req.body, req.headers, process.env.SPAVIK_WEBHOOK_SECRET);
handle(event);
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
});app.post('/webhooks/spavik', async (c) => {
const body = await c.req.arrayBuffer();
const event = await verifier(body, c.req.raw.headers, c.env.SPAVIK_WEBHOOK_SECRET);
handle(event);
return c.body(null, 200);
});The bytes, not the object
The signature covers the bytes as received. A body parsed into JSON and re-serialised does not produce the same signature, even with exactly the same data. Hence the express.raw and the arrayBuffer above.
Verification also rejects a timestamp that is too old, which blocks replaying an intercepted call. The tolerance is five minutes by default.
Suspend rather than delete
curl -X PATCH "$SPAVIK_BASE_URL/v1/webhooks/{id}" \
-H "X-API-Key: $SPAVIK_API_KEY" \
-H 'Content-Type: application/json' -d '{"active": false}'A suspended webhook keeps its secret and its configuration. Deleting it would force you to create another one, and therefore to change secret.
Each webhook's last_status field tells you what your server answered on the last call: enough to spot a broken integration without digging through logs.
One point of separation
Webhooks are managed from a web session only. An API key that tries gets a 403. That is deliberate: a key deployed inside an application must not be able to redirect your notifications.
Do not confuse the two directions
This page describes outbound webhooks: Spavik calls your server. The route POST /v1/integrations/stripe/webhook is an inbound webhook, called by Stripe, and none of your concern.