Register HTTPS endpoints and Peeve posts events to them as things happen — a
hand-off opens, a lead is created, a meeting is booked.
This is Peeve sending to you. It is not the inbound connector plumbing for
WhatsApp or Slack, which Peeve configures on your behalf and which is not a
customer surface.
Setup
Two equivalent paths — Connectors → Webhooks in the dashboard, or
the API. An endpoint created
through the API is the same row with the same guarantees, and it flips the
Connectors card to connected just the same.
Peeve issues a signing secret (whsec_…) when you create the endpoint.
The secret is returned exactly twice: on create, and on
rotate-secret. Never on a list, a
GET or a PATCH, and there is no way to recover it later. Store it when you see
it.Reads return a 4-character secret_hint instead, so you can confirm a rotation
happened without exposing the secret.
Choosing events is mandatory. POST /v1/webhooks requires events;
omitting it is a 400 naming the field, not a subscription to everything. An
explicit events: [] is refused rather than widened — in an API call an
empty array is a deliberate statement, and silently turning it into “all
events” would hand the maximum traffic to a caller who meant to narrow.Pass ["*"] if you genuinely want everything, including events added later.channels is different, and deliberately so. It is optional, and an
absent value means every channel — because a channel filter can only ever
narrow an event set you already chose. An absent channels cannot deliver
an event nobody asked for; an absent events could deliver all of them.That is the whole asymmetry: handoff.* payloads carry real customer message
text, so subscribing a new URL to them because a field was left blank is not a
convenience.
Use the test-send to confirm your receiver works before you rely on it. A test
delivery carries livemode: false and a Peeve-Test: true header, and is capped
at 20 per endpoint per hour.
Never create real records from a delivery with livemode: false. Log it and
move on.
Verifying a request — read this first
Verify before you trust. A POST to your URL is unauthenticated by default:
anyone who learns the URL can forge a hand-off. A receiver that acts on one —
opens a ticket, pages an on-call, issues a refund — has been had.The signature is the only thing that makes a request provably from Peeve.
The signed string is "<timestamp>.<raw body>". The timestamp is inside the
HMAC, so a captured request cannot be replayed with a rewritten header.
Three rules:
- Sign the raw body bytes, exactly as received. Re-serialising parsed JSON
produces different bytes and the check will fail.
- Compare in constant time.
- Reject anything older than about five minutes.
The envelope
Event catalogue
Status and channel are payload fields, never event names. There is no
handoff.resolved event — it is handoff.status.changed with
status: "resolved". Match on type, then read the field.
Status vocabularies
Retries and idempotency
Every retry carries the same id. Record the ids you have processed and
ignore repeats — that is the whole idempotency contract.Answer 2xx fast and do your work afterwards. A receiver that finishes its
processing before responding will be retried while it is still working.
Delivery log
Connectors → Webhooks → your endpoint → Recent deliveries. Each row records
the event, the channel, the outcome, your HTTP response code, the attempts taken
and how long it took.
Managing endpoints
There is no third lifecycle state; this matches the connector UI’s two
operations exactly.Reach for rotate-secret, not delete-and-recreate, when a secret leaks —
recreating changes the endpoint id and throws away its delivery history.
Destination requirements
Enforced on update as well as create, so an endpoint cannot be edited into a
private address:
- HTTPS only —
http:// is a 400.
- No
localhost, no RFC-1918 private ranges, no IPv6 loopback, no link-local
169.254.169.254, no .internal, no non-HTTP schemes.
Worth knowing before you start: a local tunnel will not work unless it
terminates on a public HTTPS address.
An endpoint whose URL later stops qualifying is marked blocked in the delivery
log.