Webhooks
Verdict changes, pushed to you
When a stock's Shariah verdict moves, we POST it to your endpoint within about a minute — signed, retried, and in the same shape as the feed you would otherwise poll. Business plans.
What a webhook is
A reversal of who calls whom. Instead of your system asking ours “anything new?” on a timer, you give us a URL once and we call you when something happens. No polling loop, no interval to tune, and no window in which a change has happened and you do not know yet.
Why it suits this feed in particular
Verdicts are recomputed nightly, Monday to Friday. A run produces somewhere between a handful and a few dozen changes, all inside a three-hour morning window — and then nothing at all for the rest of the day.
Polling that means asking a question whose answer is “no” for roughly twenty-one hours out of every twenty-four, and still learning about the morning's changes an interval late. Being pushed to costs you nothing while nothing is happening, and reaches you about a minute after it does.
Akinda analyzer
recomputes verdicts nightly, Mon–Fri
Akinda API
records the change, signs it
Your endpoint
receives it within about a minute
Setting it up
- 1
Get a Business plan
Webhooks ride the same entitlement as the verdict-change feed. Talk to us and we will set it up.
- 2
Add your endpoint
Dashboard → Webhooks. Paste an HTTPS URL and save. You can hold up to five at once.
- 3
Copy the signing secret
Shown once, on the save that creates the endpoint. Store it before you close the dialog — it is never shown again.
- 4
Verify and return 2xx
Check the signature on every delivery, then answer 2xx before doing slow work. We time out at ten seconds.
- 5
That is it
Verdict changes arrive as they happen. Nothing to poll, nothing to schedule.
What you receive
The data object is identical to an event from GET /api/v1/verdict-changes, so moving from polling to push needs no parser change. This is a real delivery — event 164, two rulebooks moving at once.
{ "type": "verdict.change", "event_seq": 164, "delivered": "2026-09-20T03:54:47Z", "attempt": 1, "replay": true, "data": { "seq": 164, "run_id": "run-2026-09-18", "ticker": "NUWE", "company_name": "Nuwellis, Inc.", "event_type": "verdict.changed", "event_class": "verdict", "changed_at": "2026-09-18T08:42:49", "note": null, "changed_statuses_by_methodologies": [ { "methodology": "djim", "new_status_djim": "HALAL", "old_status_djim": "NOT HALAL" }, { "methodology": "sp", "new_status_sp": "HALAL", "old_status_sp": "NOT HALAL" } ] } }
Note the key spelling. Each entry's status keys are suffixed with its own rulebook — new_status_djim, not new_status. Read them off entry.methodology rather than hard-coding five shapes.
History is a separate thing
A webhook delivers what happens from now on. It does not replay the past. Registering returns a start_seq; everything at or below it already happened, and you read it once from GET /api/v1/verdict-changes, which holds the last 30 days.
One detail that catches people: ?since= is a forward cursor. It returns events after the number you pass, not before it.
Verifying the signature
Every delivery carries Akinda-Signature — an HMAC-SHA256 of "<t>.<raw body>" with your secret — and Akinda-Event-Id, which is your idempotency key and is stable across redeliveries.
const crypto = require('crypto');
function verify(rawBody, header, secret) {
// Parse defensively: a forged header is attacker-controlled input.
const parts = {};
for (const p of String(header || '').split(',')) {
const i = p.indexOf('=');
if (i > 0) parts[p.slice(0, i).trim()] = p.slice(i + 1).trim();
}
if (!parts.t || !parts.v1) return false;
// Reject stale timestamps, or a captured payload replays forever.
if (!/^\d+$/.test(parts.t)) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const signed = parts.t + '.' + rawBody;
const expected = crypto.createHmac('sha256', secret)
.update(signed)
.digest('hex');
// timingSafeEqual THROWS on a length mismatch, so compare lengths first --
// otherwise a forged signature raises instead of returning false.
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Sign the raw body before parsing it, compare in constant time, and reject stale timestamps. The length check is not decoration: without it this function throws on exactly the inputs it exists to reject, and a handler that throws answers 500 — which many frameworks read as “retry later” rather than “rejected”.
When your endpoint is down
Anything outside 2xx is a failure, and we retry on a fixed ladder — real numbers, because “we retry a few times” tells you nothing about whether your maintenance window is safe:
1m → 5m → 30m → 2h → 6h → 12h
That spans a full 24 hours. If it is still failing at the end of it we stop, disable the endpoint and email you; one that recovers inside the window is never disabled. Either way nothing is lost — every event stays readable from GET /api/v1/verdict-changes.
Available on Business plans
Webhooks use the same entitlement as the verdict-change feed. The full endpoint reference, headers and payload are in the developer documentation.