pinecallDocs
Guides / Alerts

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:

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.

HTTP
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.

Next steps#