Skip to content

Webhooks

MopokeHQ can post events to your own HTTPS endpoints: a chat bridge, a ticketing system, a SIEM. Each delivery is JSON, signed with the endpoint’s secret, and retried until your endpoint answers with a 2xx status.

Settings, Integrations, Webhooks, Add endpoint. Owners and admins manage endpoints.

  • URL: https:// only, on the public internet. URLs with credentials in them, localhost, and private or link-local addresses (including cloud metadata addresses) are refused. MopokeHQ checks the address it actually connects to on every delivery, so a name that later resolves to an internal address is refused too. Redirects are not followed.
  • Events: choose which events the endpoint receives.
  • Signing secret: starts with whsec_ and is shown once. Store it where your receiver can read it. MopokeHQ keeps it encrypted with a key unique to your organisation.

An organisation can have up to 20 endpoints.

On each endpoint:

  • Send test event delivers a webhook.test event to that endpoint only.
  • Turn off stops deliveries; queued ones are marked failed.
  • Delete removes the endpoint and its delivery log.

Adding, changing and deleting endpoints are recorded in the evidence log.

Event When
signal.created An advisory, scanner finding or scan result arrived
exposure.opened An advisory matched one of your repositories
exposure.fixed A fix was seen on a branch, not yet deployed
exposure.verified A fix was confirmed live
response.closed Every exposure of an advisory is resolved
response.acknowledged A person took an advisory on
response.not_affected A person decided an advisory does not affect you
response.accepted A person accepted the risk until a review date
response.reopened A decision was undone, by a person or because it stopped holding
response.ai_overridden A person kept an advisory that Lilly suggested was not affected
fix.requested A person asked for an upgrade merge request
fix.opened Your pipeline’s job opened that merge request
fix.failed The job could not open it; the payload says why
comms.sent A client notice was approved and sent
runtime.reported A live server reported a runtime different from its last check-in, or checked in for the first time
package.quarantined The package proxy held a version for review
package.blocked The package proxy blocked a version (known malicious, over the block threshold, or kept blocked by a person)
package.released A person let a held version through
merge_request.scanned A merge request pipeline was compared with its base and the merge gate decided
merge_request.reviewed A code review of a merge request finished
project.settings_changed A project’s kind, code review choice, gate mode or owners changed
gate_policy.changed The organisation changed its merge gate policy
finding.dismissed A person dismissed a scanner finding with a reason
finding.reopened A person undid a dismissal
target.verified Ownership of an external scanning target was proved
scan.completed An external scan finished and its findings were recorded
target_finding.opened An external scan reported a new problem, or one that was fixed came back
target_finding.fixed A later scan of the same kind no longer reports it
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: MopokeHQ-Webhooks/1
MopokeHQ-Event: exposure.opened
MopokeHQ-Delivery: 1b6f...
MopokeHQ-Signature: t=1792641600,v1=5f2c...
{"id":"1b6f...","type":"exposure.opened","created_at":"2026-10-23T01:02:03Z","subject":"GHSA-...","actor":"mopoke","data":{...}}
Field Holds
id The event id, also in the MopokeHQ-Delivery header
type The event, also in the MopokeHQ-Event header
created_at When it happened, UTC
subject What it is about, such as an advisory id
actor Who caused it: mopoke for MopokeHQ itself, user:<id> for a person, or a CI source
data The event’s details. When a person caused it, data also has actor_role, their role in the organisation at the time, and actor_teams when they are in teams.

The same event can arrive more than once, for example a retry after your endpoint timed out. Use id to ignore repeats.

The MopokeHQ-Signature header is t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is computed over the string <t>.<raw body>, keyed with the signing secret as text.

To verify:

  1. Read the raw request body bytes, before any JSON parsing. Re-serialised JSON will not match.
  2. Take t and v1 from the header.
  3. Refuse the delivery if t is more than five minutes from now.
  4. Compute HMAC-SHA256 of t, a full stop, then the raw body, with your secret.
  5. Compare with v1 in constant time.

Node.js:

import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(header, rawBody, secret) {
const t = /(?:^|,)t=(\d+)/.exec(header)?.[1]
const v1 = /v1=([a-f0-9]+)/i.exec(header)?.[1]
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const want = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest()
const got = Buffer.from(v1, 'hex')
return got.length === want.length && timingSafeEqual(got, want)
}

PHP:

function mopokehq_verify(string $header, string $rawBody, string $secret): bool
{
if (!preg_match('/(?:^|,)t=(\d+)/', $header, $t) || !preg_match('/v1=([a-f0-9]+)/i', $header, $v)) {
return false;
}
if (abs(time() - (int) $t[1]) > 300) {
return false;
}
$want = hash_hmac('sha256', $t[1] . '.' . $rawBody, $secret);
return hash_equals($want, strtolower($v[1]));
}
// $rawBody = file_get_contents('php://input');

Python:

import hashlib, hmac, re, time
def verify(header: str, raw_body: bytes, secret: str) -> bool:
t = re.search(r"(?:^|,)t=(\d+)", header)
v1 = re.search(r"v1=([a-fA-F0-9]+)", header)
if not t or not v1 or abs(time.time() - int(t.group(1))) > 300:
return False
want = hmac.new(secret.encode(), t.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, v1.group(1).lower())

Answer with a 2xx status within 10 seconds. Do slow work after you answer, for example by queueing the event.

Anything else (another status, a timeout, a refused connection) is retried with exponential backoff, up to 10 attempts over roughly a day, after which the delivery is marked failed.

Each endpoint’s last 50 deliveries are on its page: status, attempts, and the last response code or the reason it failed. MopokeHQ never stores your endpoint’s response body.