Alerts
A monitor fired, the spend unusual, a quota out: the bell, and a webhook of your own, signed.
Three things happen to an org that nobody should learn from a customer: a number it watches slips, a day costs three times the usual, a quota runs out. Each is an alert: written on the agent's log as an event, told to the people who chose it in the console's bell, and posted to a URL of the org's own.
| alert | when | how often |
|---|---|---|
monitor.fired |
a monitor's number crossed its line over its window | once a day per monitor |
spend.unusual |
today's calls cost three times the org's usual day | once a day |
credits.exhausted |
a call, a written turn or a register was refused because a quota ran out | each refusal |
The bell#
Every person chooses what the console tells them, per world, under Notifications (your name, at the foot of the sidebar): a call asking for a person (on by default), every incoming call (off), and a monitor that fires (on in production, off in the sandbox). A notice reaches the browser with the console closed and the phone app; its tap opens the call, or Monitors for a monitor.
The webhook#
The org names one URL — Slack, PagerDuty, your backend — in the console at Settings ▸ Alerts or in a terminal:
printf %s "$WEBHOOK_SECRET" | pinecall webhook set https://hooks.example.com/pinecall --secret
pinecall webhook test
alerts go to https://hooks.example.com/pinecall · signed
the URL took it: a signed webhook.test post answered 2xx
Every alert is then posted as it is written, tried twice within five seconds; a URL that fails is logged and the agent's log keeps the alert either way.
POST https://hooks.example.com/pinecall
content-type: application/json
x-pinecall-event: monitor.fired
x-pinecall-signature: sha256=3f1c…
{"type":"monitor.fired","org":"org_7c2e","env":"production","agent":"front-desk","at":1791567316.2,
"data":{"monitor":"mon_3f9a1c2b4d5e","name":"slow answers","metric":"e2e_median_s","above":true,
"threshold":2.0,"value":2.41,"window_days":7,"agent":null,"env":"production","day":"2026-10-09"}}
type is the event's name; org and env say whose world it is; agent is the agent whose log
carries it (null on the test post); at is Unix seconds; data is the event itself, field for
field as the control events page writes it. webhook.test is what pinecall webhook test and the console's Send a test post, with {"said": …} as its data.
Verifying a post#
With a secret set, x-pinecall-signature is sha256= followed by the HMAC-SHA256 of the raw body,
hex-encoded. Compute it over the bytes as they came, before parsing, and compare in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
export function isPinecalls(rawBody: Buffer, signature: string | undefined, secret: string): boolean {
const ours = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
return signature !== undefined && signature.length === ours.length && timingSafeEqual(Buffer.from(signature), Buffer.from(ours));
}
import hashlib, hmac
def is_pinecalls(raw_body: bytes, signature: str | None, secret: str) -> bool:
ours = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return signature is not None and hmac.compare_digest(signature, ours)
require "openssl"
def pinecalls?(raw_body, signature, secret)
ours = "sha256=#{OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)}"
!signature.nil? && OpenSSL.secure_compare(signature, ours)
end
A post with no secret set carries no signature: anybody who knows the URL could post one, so set a secret for anything that acts on an alert.