API and webhooks
Pipemetry exposes a read-only JSON API for your own workspace’s numbers, plus outbound webhook endpoints you can register and verify.
API keys
An org admin mints keys from workspace settings. Three things to know:
- The full key is returned exactly once, at creation. Pipemetry stores only its SHA-256 hash plus a short display prefix, so it can never show you the key again — copy it then.
- Listing keys returns metadata only: prefix, name, timestamps, revoked state.
- Revoking is immediate and idempotent. A revoked key stops resolving on its very next request.
Keys look like pm_live_…. A token that does not carry that prefix is rejected at the edge without
a database lookup.
The read API
Authenticate with the key in a Bearer header:
curl -H "Authorization: Bearer pm_live_…" \ "https://app.pipemetry.com/v1/forecast?as_of_day=2026-08-07"There is no development bypass on /v1 — a valid key is required in every environment.
Machine-readable spec
GET https://app.pipemetry.com/v1/openapi.json returns an OpenAPI 3 document describing these
endpoints. It needs no key, so you can generate a client or import it into Postman before you mint
one. It is generated from the live routes, so it cannot drift from what the API actually serves.
Scopes
A key can be narrowed to the endpoints an integration actually needs, so a script that only pulls the accuracy scorecard never holds a credential that can also read every open deal.
| Scope | Grants |
|---|---|
forecast:read | GET /v1/forecast |
opportunities:read | GET /v1/opportunities |
accuracy:read | GET /v1/accuracy |
Pick scopes when you create the key. Tick none and the key gets all three — which is also what
every key created before scopes existed carries, so nothing you already built has narrowed.
Calling outside a key’s scopes returns 403 with insufficient_scope and the scope you needed.
That is deliberately not upgrade_required: a bigger plan will not help, a key with the scope will.
| Endpoint | Returns |
|---|---|
GET /v1/forecast | The forecast-category roll-up and totals |
GET /v1/opportunities | Reconstructed opportunities, plus a full-cohort win-rate block |
GET /v1/accuracy | Submitted-vs-actual scorecards (MAPE, bias, compliance) |
Money values are decimal strings on the wire, not floats — so nothing is lost to binary floating point before it reaches your spreadsheet or warehouse.
GET /v1/forecast
| Parameter | Notes |
|---|---|
as_of_day | Required. The calendar day to roll up as of |
group_by | owner (default), segment, or stage |
model_id | Optional. Omitted means your workspace’s active model. Pro |
forecast_type | all (default), new_business, renewal, expansion. Pro when not all |
measure | bookings (default) or arr. Pro when arr |
The base roll-up — owner/segment/stage, your active model, whole-cohort bookings — is available on
Starter. The three parameters marked Pro are each checked only when you actually use them, so a
Starter key gets a working forecast and a 403 upgrade_required only if it asks for a Pro
capability. An unrecognised model_id is a 422.
GET /v1/opportunities
as_of_day is required. Narrow with stage, owner, segment, forecast_category, deal_type
(new_business / renewal / expansion) or q (free text over name and account). Page with
limit (1–200, default 50) and offset, and sort with sort + dir. An unsupported sort field
is a 422 — the sortable set is curated server-side.
The response also carries win_rate, settled_won and settled_lost, computed over every
settled deal the key can see — not just the page you requested. settled_unknown counts deals
whose closed status could not be determined; when it is above zero, win_rate is null rather
than a rate computed over a partial set.
GET /v1/accuracy
as_of_day is required. periods selects how many closed periods to score (default 4, max 8), and
group_by is owner or manager. model_id is accepted here too, and scores the AI line under
that model; an unrecognised value is a 422. See
Accuracy and backtesting for what the fields mean.
Two capabilities on this endpoint are sold on Pro — group_by=manager (manager roll-up) and
choosing a model — and both are now enforced here, exactly as on /v1/forecast: a key whose
workspace is not on a plan that grants them gets a 403 with an upgrade_required body naming the
missing flag. The default scorecard (group_by=owner, no model_id) is unaffected on every plan.
Rate limits
120 requests per minute per API key by default. Both caveats this section used to carry are
gone: the budget is now per key rather than a shared ceiling, and /v1 no longer draws on the same
bucket as the app’s own browser reads — one integration can neither throttle another nor slow the
app down.
Every response — allowed or rejected — carries X-RateLimit-Limit and X-RateLimit-Remaining, and
a 429 also carries Retry-After: 60. Pace yourself from the remaining budget rather than polling
hard and backing off after the fact.
Webhooks
An org admin registers an HTTPS endpoint per workspace.
- Register returns the signing secret (
whsec_…) once. Store it on your receiver; it is never shown again. - The URL is validated before it is accepted: HTTPS only, and no loopback or private-network hosts.
A rejected URL is a
422 invalid_webhook_url. This is what stops a registered webhook being used to reach internal addresses. - Listing returns metadata only — URL, description, active flag, last delivery state — never the secret.
- Send test dispatches a signed
test.pingenvelope to every active endpoint and reports each one’s outcome, so you can prove your receiver works end to end.
The envelope
{ "id": "…", "type": "test.ping", "workspace_id": "…", "created_at": "…", "data": {}}Verifying a delivery
Each POST carries a Pipemetry-Signature header in Stripe’s format:
Pipemetry-Signature: t=<unix-timestamp>,v1=<hex-hmac-sha256>To verify:
- Read
tandv1from the header. - Compute
HMAC-SHA256(secret, "<t>.<raw request body>")and hex-encode it. - Compare it to
v1using a constant-time comparison. - Reject the delivery if
tis more than 300 seconds away from your clock — that is the replay window Pipemetry’s own verifier uses.
Use the raw body bytes as received. Pipemetry serialises the envelope canonically — JSON with sorted keys and no whitespace — so re-serialising a parsed object will reproduce the same bytes, but reading the raw body is the safer habit.
Error responses
| Status | Meaning |
|---|---|
401 | Missing Bearer token, or a key that is malformed, unknown or revoked |
402 payment_required | Billing is suspended for the workspace |
403 upgrade_required | The plan does not grant the capability you asked for |
422 | Invalid input — an unknown model_id, an unsupported sort, a rejected webhook URL |
429 | Rate limited; retry after the Retry-After interval |
503 | The service is not able to serve that read right now |