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.
Set one up
Section titled “Set one up”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.testevent 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.
Events
Section titled “Events”| 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 |
What a delivery looks like
Section titled “What a delivery looks like”POST /your/endpoint HTTP/1.1Content-Type: application/jsonUser-Agent: MopokeHQ-Webhooks/1MopokeHQ-Event: exposure.openedMopokeHQ-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.
Check the signature
Section titled “Check the signature”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:
- Read the raw request body bytes, before any JSON parsing. Re-serialised JSON will not match.
- Take
tandv1from the header. - Refuse the delivery if
tis more than five minutes from now. - Compute HMAC-SHA256 of
t, a full stop, then the raw body, with your secret. - Compare with
v1in 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())Retries and the delivery log
Section titled “Retries and the delivery log”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.