Webhooks
Webhooks deliver platform events to an HTTPS URL you control. Register a subscription with POST /api/v1/webhooks, subscribe to one or more events, and we'll POST each matching event to your URL with an HMAC signature.
Subscribing
POST /api/v1/webhooks
Authorization: Bearer psg_live_...
X-Org-Id: <uuid>
Idempotency-Key: <uuid>
Content-Type: application/json
{
"url": "https://example.com/psg-webhook",
"events": ["defect.reported", "check.completed"],
"description": "Compliance team integration"
}You receive a secret once in the response. Store it; it's used to verify delivery signatures and cannot be retrieved later.
URL requirements
The URL must:
- use HTTPS
- use a host name, not an IP address
- not contain a user name or password
- have an IPv4 address
- resolve only to public IP addresses
The host name needs at least one IPv4 address (an A record): one with only IPv6 (AAAA) records is refused. The last rule applies to every address the host name resolves to, IPv4 and IPv6: a single private, loopback, link-local or other special-use address refuses the URL. PreStartGo checks these rules when you register or change the URL, and again before every delivery. A registration or change that breaks a rule is refused with a 400 that names it, except that a host name that does not resolve and one that resolves to an address that is not public get the same generic answer, by design.
Rotating the secret
If the secret is lost or exposed, call POST /api/v1/webhooks/{id}/rotate-secret (scope webhooks:write). The response carries a new secret, once, and the old one stops working immediately: every delivery from then on, including retries of earlier events, is signed with the new secret. Update your endpoint as soon as you rotate. Until it has the new secret, a 401 or 403 it returns within 24 hours of the rotation is retried rather than failed, so those deliveries are not lost.
Event catalogue
Subscribe to specific events by name, or use * (wildcard) to receive all current and future events.
| Event | Resource | Description |
|---|---|---|
check.completed | Check | A pre-start safety check was completed and synced from the mobile app. Payload includes the check outcome (PASS, PASS_WITH_DEFECTS, FAIL, INCOMPLETE). |
defect.reported | Defect | A defect was raised against a piece of equipment: by a person in the mobile app, the admin console or the API; for an item failed on a pre-start check, including the defect PreStartGo raises itself when the operator records none; or for a critical item failed on a service checklist. |
service.completed | Service | A maintenance service finished (status transitioned to COMPLETED). Useful for firing uptime-reporting or CMMS integrations. |
Delivery shape
POST https://your-endpoint/...
Content-Type: application/json
User-Agent: PreStartGo-Webhooks/1.0
X-PSG-Event: defect.reported
X-PSG-Delivery: <uuid>
X-PSG-Timestamp: 1713600000
X-PSG-Signature: sha256=<hex digest>
{
"eventType": "defect.reported",
"orgId": "<uuid>",
"resourceType": "DEFECT",
"resourceId": "<uuid>",
"data": { ... },
"timestamp": "2026-04-20T14:22:03.000Z"
}The body has no id field: each delivery's id is the X-PSG-Delivery header. X-PSG-Timestamp is the Unix time, in seconds, at which that attempt was signed.
Signature verification
Every delivery is signed with HMAC-SHA256 under your webhook secret. The X-PSG-Signature header is sha256= followed by the lowercase hex digest of timestamp + "." + rawBody, where timestamp is the X-PSG-Timestamp header's value and rawBody is the request body exactly as received, before any JSON parsing. Recompute that value with your secret and compare it with the header in constant time.
Reject a timestamp more than 5 minutes from your own clock, to prevent replay. Every retry is signed afresh with a new timestamp, so a retried delivery still passes. In Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isGenuine(rawBody, headers, secret) {
const timestamp = headers['x-psg-timestamp'];
const received = headers['x-psg-signature'] ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
return (
fresh &&
received.length === expected.length &&
timingSafeEqual(Buffer.from(received), Buffer.from(expected))
);
}Delivery semantics
- At-least-once delivery: the same delivery can arrive more than once. De-duplicate on
X-PSG-Delivery, which is the same on every attempt at one delivery. - A 2xx response within 10 seconds confirms delivery.
- A 4xx response other than 408, 425 or 429 fails the delivery permanently, with no further retry, so answer 4xx only when a retry can never succeed. The one exception is a 401 or 403 within 24 hours of the signing secret being issued (the webhook created, or its secret rotated), which is retried.
- A 408, 425 or 429 response, any 5xx, a timeout or a connection error is retried with exponential backoff, capped at 1 hour between attempts: up to 20 attempts in all, over about 10 hours.
- PreStartGo does not follow redirects: any 3xx response fails the delivery permanently, with no further retry, so register your endpoint's final URL.
- A delivery to a URL that breaks any of the URL requirements but the last two fails permanently, with no further retry. If the host name does not resolve, has no IPv4 address, or resolves to an address that is not public, the attempt fails and is retried, since DNS can change.
- Webhook delivery history is queryable via
GET /api/v1/webhooks/{id}/deliveries.