POST
/api/ping/{pingUrl}{pingUrl} is the monitor's unique identifier — find it on the monitor detail page. Both POST and GET are accepted; POST with a JSON body is the recommended form.
Headers
| Field | Type | Description |
|---|---|---|
X-Webhook-Signaturerequired | string | HMAC-SHA256 of the request body hex-encoded with the team webhook secret. See Authentication. |
Content-Type | application/json | Required when sending a JSON body. |
Body
The body is optional. When omitted, the ping is treated as status: "up".
| Field | Type | Description |
|---|---|---|
status | "up" | "down" | Defaults to "up". Sending "down" raises an incident immediately. |
reason | string | Optional reason string, max 200 chars. Persists on the ping row and drives down→down dedup. |
group_key | string | Routing hint used at alert-firing time, max 200 chars. Not persisted. |
metadata | object | Arbitrary key-value payload. If you POST extra top-level fields, they are folded into metadata automatically. |
Example request
BODY='{"status":"up","metadata":{"batch":1247,"durationMs":832}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$OPSHIFT_SECRET" -hex | awk '{print $2}')
curl -X POST https://www.opshift.io/api/ping/payments-worker \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: $SIG" \
-d "$BODY"import { createHmac } from "node:crypto";
const secret = process.env.OPSHIFT_SECRET;
const body = JSON.stringify({
status: "up",
metadata: { batch: 1247, durationMs: 832 },
});
const signature = createHmac("sha256", secret).update(body).digest("hex");
const res = await fetch(
"https://www.opshift.io/api/ping/payments-worker",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Webhook-Signature": signature,
},
body,
},
);
console.log(await res.json());import hashlib
import hmac
import json
import os
import urllib.request
secret = os.environ["OPSHIFT_SECRET"]
body = json.dumps({
"status": "up",
"metadata": {"batch": 1247, "durationMs": 832},
}).encode()
signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
request = urllib.request.Request(
"https://www.opshift.io/api/ping/payments-worker",
data=body,
method="POST",
headers={
"Content-Type": "application/json",
"X-Webhook-Signature": signature,
},
)
with urllib.request.urlopen(request) as response:
print(response.read().decode())package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
)
func main() {
secret := os.Getenv("OPSHIFT_SECRET")
body := []byte(`{"status":"up","metadata":{"batch":1247,"durationMs":832}}`)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
signature := hex.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest(
http.MethodPost,
"https://www.opshift.io/api/ping/payments-worker",
bytes.NewReader(body),
)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Webhook-Signature", signature)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(string(out))
}use hmac::{Hmac, Mac};
use sha2::Sha256;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let secret = std::env::var("OPSHIFT_SECRET")?;
let body = r#"{"status":"up","metadata":{"batch":1247,"durationMs":832}}"#;
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())?;
mac.update(body.as_bytes());
let signature = hex::encode(mac.finalize().into_bytes());
let response = reqwest::Client::new()
.post("https://www.opshift.io/api/ping/payments-worker")
.header("Content-Type", "application/json")
.header("X-Webhook-Signature", signature)
.body(body)
.send()
.await?;
println!("{}", response.text().await?);
Ok(())
}Response
Returns 200 OK on success.
json
{
"message": "Ping received",
"monitor": {
"name": "Payments worker",
"status": "up",
"monitoringActive": true
},
"pingStatus": "up"
}Response fields
| Field | Type | Description |
|---|---|---|
message | string | Human-readable summary. First-ever ping returns "First ping received — monitoring started"; first ping after a resume returns "Ping received — monitoring restarted". |
monitor.name | string | The monitor's display name. |
monitor.status | "idle" | "up" | "down" | "paused" | Effective monitor status after this ping. |
monitor.monitoringActive | boolean | True once monitoring has started (first ping ever or first ping after resume). |
pingStatus | "up" | "down" | The status that this ping reported. |
Errors
| Field | Type | Description |
|---|---|---|
401 | Unauthorized | Missing or invalid signature. |
403 | Forbidden | Team subscription expired. |
404 | Not found | Unknown ping URL, or the monitor is paused. |
429 | Too many requests | Rate limit exceeded (max 10 pings per minute per ping URL) or monthly heartbeat quota exhausted. |
Behaviour during a ClickHouse outage
If the analytics database is briefly unreachable, the ping is still accepted with default monitor state. This is intentional — a backend blip should not silently drop your monitoring data.