Skip to content

Receive alerts with a signed webhook

Have Gryphon post every alert to your own HTTPS endpoint as JSON, signed so you can tell it came from Gryphon: the payload, the headers, and how to check the signature.

Integration you'll add
Webhook
The agent
No agent needed
Written for
Any system

What you'll get

  • A POST to your address for every alert: problems, warnings, recoveries and acknowledgements, as JSON, by the same rules as every other channel.
  • A signature on every request, made with a secret only you and Gryphon hold, so your receiver can refuse anything Gryphon did not send, and anything replayed later.
  • Somewhere to start anything: open a ticket, page through your own tool, light a lamp, post to a chat Gryphon has no integration for.

Before you start

  • An HTTPS address on the public internet that accepts a POST and answers within ten seconds. Plain HTTP is refused: the body names your hosts.
  • In Gryphon, the owner of the account, or a member the owner has allowed to change hosts.

Steps

1Add the webhook

On the dashboard, open Integrations and choose Webhook. In the iPhone app it is in the account menu; on Android, under Settings.

Add a webhook Integrations → Add an integration
Name
Incident bot
Webhook URL
https://example.com/gryphon
What it sends
Problems, Warnings, Recoveries, Acknowledgements
Which hosts
Every host, or only the hosts you choose

2Copy the signing secret

Saving takes you to the webhook's page with its signing secret showing, a string starting whsec_. Copy it into your receiver's configuration, for instance as GRYPHON_WEBHOOK_SECRET. It can be shown again on that page, and Make a new secret replaces it: the old one stops working at once.

3Check the signature in your receiver

Every request carries a Gryphon-Signature header like t=1791310863,v1=5b2c…. t is when it was signed, in Unix seconds; v1 is the hex HMAC-SHA256 of t, a full stop, and the raw body, under your secret. Recompute it over the body exactly as it arrived (before any JSON parsing), compare in constant time, and refuse a time more than a few minutes away. Each of these was checked against a request Gryphon signed.

Go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"math"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

var secret = []byte(os.Getenv("GRYPHON_WEBHOOK_SECRET"))

// verify reports whether a request came from Gryphon, recently.
func verify(header string, body []byte) bool {
	var ts, sig string
	for _, part := range strings.Split(header, ",") {
		k, v, _ := strings.Cut(part, "=")
		switch k {
		case "t":
			ts = v
		case "v1":
			sig = v
		}
	}
	t, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 {
		return false // no time, or more than five minutes from now: refuse a replay
	}
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	want := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(want), []byte(sig))
}

func main() {
	http.HandleFunc("POST /gryphon", func(w http.ResponseWriter, r *http.Request) {
		body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
		if !verify(r.Header.Get("Gryphon-Signature"), body) {
			http.Error(w, "bad signature", http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusNoContent)
		// ... act on the alert in body
	})
	http.ListenAndServe(":8080", nil)
}
Node.js (Express)
const crypto = require("crypto");
const express = require("express");
const secret = process.env.GRYPHON_WEBHOOK_SECRET;

// verify reports whether a request came from Gryphon, recently.
function verify(header, body) {
  const parts = Object.fromEntries((header || "").split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.v1 || !(age <= 300)) return false;
  const want = crypto.createHmac("sha256", secret).update(parts.t + ".").update(body).digest("hex");
  return want.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}

const app = express();
// The raw body: the signature is over the bytes Gryphon sent.
app.post("/gryphon", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.get("Gryphon-Signature"), req.body)) return res.status(401).send("bad signature");
  res.sendStatus(204);
  const alert = JSON.parse(req.body);
  // ... act on alert.event, alert.host, alert.check
});
app.listen(8080);
Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, abort, request

SECRET = os.environ["GRYPHON_WEBHOOK_SECRET"].encode()
app = Flask(__name__)

def verify(header, body):
    """Whether a request came from Gryphon, recently."""
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        fresh = abs(time.time() - int(parts.get("t", ""))) <= 300
    except ValueError:
        fresh = False
    want = hmac.new(SECRET, parts.get("t", "").encode() + b"." + body, hashlib.sha256).hexdigest()
    return fresh and hmac.compare_digest(want, parts.get("v1", ""))

@app.post("/gryphon")
def gryphon():
    if not verify(request.headers.get("Gryphon-Signature", ""), request.get_data()):
        abort(401)
    alert = request.get_json()
    # ... act on alert["event"], alert["host"], alert["check"]
    return "", 204

4Send a test

Press Test on the webhook. Your receiver gets a request whose event is test, and Gryphon shows what your receiver answered. A test is tried once, so the answer comes back within ten seconds.

What arrives

POST /gryphon
Content-Type: application/json
User-Agent: Gryphon-Webhook/1
Gryphon-Event: problem
Gryphon-Delivery: 6f1c0d2e-4b1a-4c55-9a2e-0d0c6b9f3a11
Gryphon-Signature: t=1791310863,v1=5b2c...

{
  "version": 1,
  "event": "problem",
  "delivery_id": "6f1c0d2e-4b1a-4c55-9a2e-0d0c6b9f3a11",
  "occurred_at": "2026-10-06T18:21:03Z",
  "account_id": 12,
  "event_id": 98123,
  "title": "Problem: HTTPS on web-01",
  "host": { "id": 7, "name": "web-01", "url": "https://.../admin/host/7#services" },
  "check": { "id": 311, "name": "HTTPS", "status": "problem", "previous_status": "healthy" },
  "message": "Down from every location: connection refused",
  "acknowledged_by": null
}
  • event (and the Gryphon-Event header) is problem, warning, recovered, acknowledged or test.
  • check.status is problem, warning or healthy. message is what the check said: the reason, the regions it is down from, the days left on a certificate.
  • On an acknowledgement, acknowledged_by is the person's name and event_id is the problem they acknowledged. A test has no host or check.
  • version is 1. A change that would break a receiver will be a new version, never an edit of this one. Ignore fields you do not know.

How it is delivered

  • Answer with any 2xx, within ten seconds. Do the slow work after answering.
  • Retries. An alert is tried up to three times: at once, two seconds later, and ten seconds after that. A 5xx, a 408, a timeout or a refused connection is retried; a 429 waits out your Retry-After (up to fifteen seconds). Any other 4xx is not retried: it would get the same answer.
  • Duplicates. Every attempt of one delivery carries the same Gryphon-Delivery, so drop one you have already seen.
  • Order. Alerts are sent as they happen, and a check that flaps can, rarely, have its recovery arrive before its problem. Order by occurred_at if it matters.
  • No redirects. A 3xx is a failure; give Gryphon the address it redirects to.
  • Public addresses only. An address on a private network or this machine is refused, checked again on what the name resolves to each time a request is sent.

If it stops working

When every attempt fails, Gryphon marks the webhook Failing on the Integrations page with your receiver's answer, and emails the people who manage the account once, not once per alert. After a week with no successful delivery it pauses the webhook and says so. Fix the receiver, send a test, and resume it: a test that arrives resumes it too.

Not what you run? Browse every guide, or tell us what you need to watch and we'll write it up.

Fourteen days free. Then from $4.99 a month.

The agent, the dashboard, the apps and every check but the five for Kubernetes are in every plan. The plans differ in how much you watch, how often, from where, and how many people and status pages they include. Compare the plans. Cancel any time.

Already have an account? Sign in