Webhooks
DevSecure sends an HTTPS POST to your endpoint when something happens in
the workspace. Events usually arrive within a minute.
Events
Section titled “Events”| Event | When |
|---|---|
scan.completed | A scan finished. Includes issue counts by severity. Subscribing needs the `scans:read` scope. |
scan.failed | A scan failed. Subscribing needs the `scans:read` scope. |
issue.created | A scan found an issue whose fingerprint was not seen before in the workspace. Subscribing needs the `issues:read` scope. |
issue.status_changed | An issue was resolved, reopened, snoozed, marked false positive or risk-accepted, in the dashboard or through this API. Subscribing needs the `issues:read` scope. |
exception_request.decided | An exception request was approved, denied or cancelled. Subscribing needs the `issues:read` scope. |
autofix.completed | An autofix job finished (fixed, low confidence or cannot fix). Subscribing needs the `autofix:read` scope. |
autofix.failed | An autofix job failed. Subscribing needs the `autofix:read` scope. |
kubernetes.finding.created | The Kubernetes agent reported a new finding. Subscribing needs the `kubernetes:read` scope. |
pentest.target.decided | A pentest target was approved or denied. Subscribing needs the `pentest:read` scope. |
ping | A test event sent by POST /v1/webhooks/{webhook_id}/test. Subscribing needs the `webhooks:manage` scope. |
The API reference shows the exact body of each event. data holds
the same objects, with the same fields, as the matching GET endpoint.
Create a webhook
Section titled “Create a webhook”curl -s https://api.devsecure.io/v1/webhooks \ -H "Authorization: Bearer $DEVSECURE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://hooks.example.com/devsecure", "event_types": ["scan.completed", "issue.created"]}'The response includes the signing secret (whsec_…) once. Store it with
your receiver.
- Managing webhooks needs the
webhooks:managescope. Each event type also needs its read scope, such asissues:readforissue.created, so a webhook never carries data its key could not read. - The URL must use
httpson a public host, with no user name or password in it. Private, loopback and cloud metadata addresses are refused, both when you create the webhook and at every delivery. - A workspace can have up to 10 webhooks. They belong to the workspace, not to the key that created them.
GET /v1/webhooks/event-typeslists the event types and their scopes.
What a delivery looks like
Section titled “What a delivery looks like”POST /devsecure HTTP/1.1Host: hooks.example.comContent-Type: application/jsonUser-Agent: DevSecure-Webhooks/1.0X-DevSecure-Event: scan.completedX-DevSecure-Event-Id: 3f2b9c1e-7a6d-4e5f-9b8a-1c2d3e4f5a6bX-DevSecure-Delivery: 0d1e2f3a-4b5c-4d6e-8f9a-0b1c2d3e4f5aX-DevSecure-Webhook-Id: 9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6aX-DevSecure-Signature: t=1790000000,v1=06ae881a…
{"api_version":"v1","created_at":"2026-10-01T12:00:00Z","data":{"scan":{…}},"id":"3f2b9c1e-7a6d-4e5f-9b8a-1c2d3e4f5a6b","type":"scan.completed"}Verify the signature
Section titled “Verify the signature”Always verify X-DevSecure-Signature before trusting a delivery. The header
holds a Unix timestamp t and one or more v1 signatures. Each signature is
the hex HMAC-SHA256 of "<t>.<raw body>", keyed with your signing secret.
- Use the raw request body, byte for byte, before any JSON parsing.
- Compute the HMAC with your secret and compare it to each
v1value in constant time. The delivery is genuine if any of them matches. - Reject timestamps more than five minutes from your clock, so an old delivery cannot be replayed.
"""Verify a DevSecure webhook delivery (Python 3.9+, standard library only)."""
import hashlibimport hmacimport time
def verify_devsecure_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool: """True when X-DevSecure-Signature matches the raw request body.
secret: the webhook's signing secret (whsec_...) header: the X-DevSecure-Signature header, "t=<unix seconds>,v1=<hex>[,v1=<hex>]" raw_body: the request body exactly as received, before any JSON parsing """ try: pairs = [part.split("=", 1) for part in header.split(",")] timestamp = int(next(value for key, value in pairs if key == "t")) signatures = [value for key, value in pairs if key == "v1"] except (StopIteration, ValueError): return False if abs(time.time() - timestamp) > tolerance: return False # too old (or too far in the future): possibly a replay signed = str(timestamp).encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, candidate) for candidate in signatures)// Verify a DevSecure webhook delivery (Node 18+, no dependencies).import { createHmac, timingSafeEqual } from 'node:crypto'
/** * @param {string} secret the webhook's signing secret (whsec_...) * @param {string} header X-DevSecure-Signature: "t=<unix seconds>,v1=<hex>[,v1=<hex>]" * @param {Buffer} rawBody the request body exactly as received, before JSON.parse * @param {number} tolerance maximum age in seconds */export function verifyDevSecureSignature(secret, header, rawBody, tolerance = 300, now = Date.now() / 1000) { const pairs = header.split(',').map((part) => { const i = part.indexOf('=') return [part.slice(0, i), part.slice(i + 1)] }) const timestamp = Number(pairs.find(([key]) => key === 't')?.[1]) if (!Number.isInteger(timestamp) || Math.abs(now - timestamp) > tolerance) return false const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex') return pairs .filter(([key]) => key === 'v1') .some(([, candidate]) => candidate.length === expected.length && timingSafeEqual(Buffer.from(candidate), Buffer.from(expected)))}These samples are tested against deliveries signed by DevSecure’s own code.
Respond quickly
Section titled “Respond quickly”- Return any
2xxstatus within 10 seconds. Do the work afterwards, for example from a queue. - Anything else, including a timeout, a TLS error or a redirect, counts as a failure. Redirects are never followed.
- Failed deliveries are retried after about 1 minute, 5 minutes, 30 minutes,
2 hours and 6 hours. After six failed attempts the delivery is marked
failed. - Respond
410 Goneto tell DevSecure to turn the webhook off. - After 20 failed deliveries in a row, DevSecure turns the webhook off and
sets
disabled_reason. Turn it back on withPATCH /v1/webhooks/{webhook_id}and{"enabled": true}.
Handle duplicates and order
Section titled “Handle duplicates and order”- An event can arrive more than once, for example after a timeout or a
redelivery. Use
X-DevSecure-Event-Id(the body’sid) to ignore duplicates. - Events can arrive out of order. Use timestamps in
data(such asupdated_at), or fetch the object from the API, when order matters.
Test, inspect and redeliver
Section titled “Test, inspect and redeliver”POST /v1/webhooks/{webhook_id}/testsends apingevent.GET /v1/webhooks/{webhook_id}/deliverieslists recent deliveries with their status, attempts, response status, error and duration. Your endpoint’s response bodies are never stored.POST /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliversends a finished delivery again.
Rotate the secret
Section titled “Rotate the secret”POST /v1/webhooks/{webhook_id}/rotate-secret returns a new secret, once.
For the next 24 hours every delivery carries two v1 signatures, one for
each secret, so you can update your receiver without dropping events.