urlyze docs

Notifications

Instead of polling every scan, let Urlyze call your endpoint when a scan finishes.

The scan.completed webhook

Once it is on, Urlyze sends a POST to your workspace's webhook URL for every scan your workspace submits, whether through POST /UrlCheck or POST /UrlCheck/batch:

POST https://your-endpoint.example.com/urlyze
Content-Type: application/json
X-Urlyze-Signature: sha256=<64 lower-case hex characters>

{
  "type": "scan.completed",
  "analyzeId": "3f6c0f2e-…",
  "url": "https://login-example.com/verify",
  "verdict": "Malicious",
  "tags": ["sys:clickfix"],
  "completedAt": "2026-10-02T12:00:00Z",
  "resultPath": "/UrlCheck/3f6c0f2e-…"
}
fieldmeaning
typealways scan.completed for this event
analyzeIdthe id POST /UrlCheck or the batch answered with
urlthe scanned URL
verdictone of the verdict values
tagsthe scan's tags
completedAtwhen the scan finished (UTC)
resultPaththe path of the full result on https://api.urlyze.io
The event is a pointer, not the result. Fetch resultPath with your own API key to read the scan. Anyone who learns your webhook URL can send you a request, so nothing in the event is enough on its own to act on.

Turning it on

The event is opt-in. Set webhook.scanCompleted to true with PUT /api/notifications/settings. The webhook URL must be an absolute https URL. Set a signing secret as well: it is the only way to tell Urlyze's calls from anyone else's.

PUT replaces the whole settings object (email, Slack, SIEM and webhook), so read the current settings first and send them back with the one change:

curl -s https://api.urlyze.io/api/notifications/settings -H "X-Api-Key: YOUR_API_KEY" \
  | jq '.webhook.scanCompleted = true | del(.updatedAt, .webhook.hasSecret, .siem.hasSecret)' \
  | curl -X PUT https://api.urlyze.io/api/notifications/settings \
      -H "Content-Type: application/json" -H "X-Api-Key: YOUR_API_KEY" -d @-

Verifying the signature

X-Urlyze-Signature is sha256= followed by the lower-case hex HMAC-SHA256 of the raw request body, keyed with your webhook secret. Compute it over the bytes you received, before any JSON parsing, and compare in constant time:

// Node.js (Express): keep the raw body for this route
app.post('/urlyze', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.URLYZE_WEBHOOK_SECRET)
    .update(req.body).digest('hex');
  const got = req.get('X-Urlyze-Signature') ?? '';
  if (got.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  queue.push(event.analyzeId);   // fetch event.resultPath later, not inside this request
  res.sendStatus(204);
});
# Python
import hmac, hashlib

def is_from_urlyze(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Delivery

This event is separate from the IOC push to your SIEM, which stays driven by confirmations; see IOC feeds.