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. 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. 2

    Add your endpoint

    Dashboard → Webhooks. Paste an HTTPS URL and save. You can hold up to five at once.

  3. 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. 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. 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

Verify every delivery before acting on it. Without it, anyone who learns your URL can POST a forged “this stock is now NOT HALAL” into your compliance system, and you cannot tell it from us.

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.