Outbound Webhooks
Outbound Webhooks
Outbound webhooks let SwiftReporter notify your own systems when something important happens in your account. When an inspection is completed, an invoice is paid, or a client books through your public booking page, SwiftReporter sends a signed JSON POST request to the HTTPS URL you configure.
Use webhooks to update a CRM, post to a team channel, start a follow-up email sequence, or trigger a Zapier workflow.
Outbound webhooks are an included capability. There is no per-delivery charge or usage meter.
Before you begin
- Have a publicly reachable
https://URL that acceptsPOSTrequests with a JSON body. - In the SwiftReporter web app, open Account (profile menu or
/account) and use the Webhooks card to add an endpoint. Choose which events it should receive. - Copy the endpoint's signing secret when it is shown. It is displayed only once, so store it somewhere safe, such as your server's environment variables.
Events catalog
| Event | When it is sent |
|---|---|
inspection.completed | An inspection's status changes to Complete. |
invoice.paid | An invoice's status changes to Paid. |
booking.created | A new appointment is created through your public booking page. |
Event IDs are based on the record, such as inspection.completed:<inspection_id>. If an inspection is reopened and completed again, or an invoice returns to Paid, the new event reuses the same ID.
Request format
Every delivery is an HTTP POST with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | SwiftReporter-Webhooks/1.0 |
X-SwiftReporter-Event | The event type, such as invoice.paid. |
X-SwiftReporter-Delivery | The event ID. Matches the id field in the body. |
X-SwiftReporter-Signature | t=<unix_seconds>,v1=<hex_hmac> (see signature verification). |
The body is a JSON envelope:
id: Unique event ID. Stays the same across every retry of the same event.type: The event type.created_at: ISO 8601 timestamp when the event was created.owner_id: The SwiftReporter account the event belongs to.data: Event-specific fields. Optional fields are left out when the source record does not have a value.
Payload examples
inspection.completed
{
"id": "inspection.completed:insp_seed_1",
"type": "inspection.completed",
"created_at": "2026-09-27T07:42:42.970Z",
"owner_id": "owner_seed",
"data": {
"inspection_id": "insp_seed_1",
"name": "123 Main St",
"client_name": "Jordan Smith",
"status": "complete"
}
}
data fields: inspection_id, status (always complete), and optional name, client_name, and org_id.
invoice.paid
{
"id": "invoice.paid:inv_seed_1",
"type": "invoice.paid",
"created_at": "2026-09-27T07:42:42.970Z",
"owner_id": "owner_seed",
"data": {
"invoice_id": "inv_seed_1",
"inspection_id": "insp_seed_1",
"total": 25000,
"currency": "USD",
"paid_at": "2026-09-27T07:42:42.900Z"
}
}
data fields: invoice_id, paid_at, and optional inspection_id, appointment_id, total, and currency. total is an integer in the smallest currency unit, so 25000 in USD is $250.00.
booking.created
{
"id": "booking.created:appt_seed_1",
"type": "booking.created",
"created_at": "2026-09-27T07:42:42.970Z",
"owner_id": "owner_seed",
"data": {
"appointment_id": "appt_seed_1",
"source": "public_booking",
"start_at": "2026-10-01T15:00:00.000Z",
"timezone": "America/Chicago"
}
}
data fields: appointment_id, source (always public_booking), and optional inspection_id, start_at, and timezone.
Verify the signature
Every request is signed with your endpoint's signing secret using HMAC-SHA256. Verify the signature before trusting the payload.
- Read the raw request body exactly as received. Do not parse and re-serialize the JSON first, because any change to spacing or key order breaks the signature.
- Split the
X-SwiftReporter-Signatureheader on,and read thet(timestamp) andv1(signature) values. - Reject the request if
tis more than 300 seconds (5 minutes) away from your server's current time. This blocks replayed requests. - Build the signed content as
${t}.${rawBody}, the timestamp, a period, then the raw body. - Compute
HMAC_SHA256(signing_secret, signed_content)as lowercase hex. - Compare your result with
v1using a constant-time comparison. Reject the request if they do not match.
Each retry is signed again with a fresh timestamp, so a delayed retry still passes the 5-minute check.
Node.js example
const crypto = require('crypto')
/**
* Verifies a SwiftReporter webhook signature.
* @param {string} rawBody - The exact request body string.
* @param {string} header - The X-SwiftReporter-Signature header value.
* @param {string} secret - The endpoint signing secret.
* @returns {boolean} True when the signature is valid and recent.
*/
function verifySwiftReporterSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header || '')
.split(',')
.map((part) => part.trim().split('='))
)
const t = Number(parts.t)
const v1 = String(parts.v1 || '').toLowerCase()
if (!Number.isFinite(t) || !/^[0-9a-f]+$/.test(v1)) return false
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - t) > 300) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`, 'utf8').digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(v1, 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
With Express, capture the raw body using express.raw({ type: 'application/json' }) on the webhook route and pass req.body.toString('utf8') to the function.
Respond quickly
- Return any
2xxstatus as soon as you have verified and stored the event. - Do slow work, such as calling other APIs, after you respond or in a background job.
- SwiftReporter waits up to 10 seconds for each attempt before treating it as a timeout.
Retries and backoff
If a delivery fails, SwiftReporter retries it up to 4 attempts in total:
| Attempt | When it is sent |
|---|---|
| 1 | Immediately |
| 2 | About 1 second after attempt 1 |
| 3 | About 4 seconds after attempt 2 |
| 4 | About 16 seconds after attempt 3 |
Retried: network errors (DNS failures, refused or reset connections), timeouts, 408 Request Timeout, 429 Too Many Requests, and any 5xx response.
Not retried: any 2xx response (success), and any other 4xx response such as 400, 401, 403, or 404. A 4xx usually means the URL, authentication, or handler needs to be fixed.
If you have several endpoints subscribed to the same event, each one is delivered and retried independently.
Handle duplicates
Retries mean your endpoint can receive the same event more than once, for example when your server processed a request but the response timed out.
Use the X-SwiftReporter-Delivery header (the same value as the body's id) as an idempotency key:
- Before processing, check whether you have already stored this delivery ID.
- If you have, return
200and skip processing. - If you have not, process the event and store the ID.
Because event IDs are per record, a repeat completion of the same record also looks like a duplicate. If you need to act on repeats, use the delivery ID together with created_at, which stays the same across retries but changes for each new event.
Use with Zapier
You can connect outbound webhooks to Zapier without a dedicated SwiftReporter Zapier app:
- In Zapier, create a Zap and choose Webhooks by Zapier → Catch Hook as the trigger.
- Copy the webhook URL Zapier gives you.
- In SwiftReporter, open Account → Webhooks, add that URL as an endpoint, and select the events you want.
- Trigger a test event, such as completing a test inspection, so Zapier can read a sample payload.
- Map fields from
datainto the rest of your Zap.
Zapier's Catch Hook does not verify the X-SwiftReporter-Signature header. If you need signature verification, send webhooks to your own server first, verify them, and forward them from there.
Webhooks compared to MCP
Outbound webhooks and SwiftReporter MCP solve different problems:
- Outbound webhooks push event notifications from SwiftReporter to your systems automatically.
- SwiftReporter MCP lets AI apps such as Cursor, Claude, ChatGPT, or Gemini read and act on your SwiftReporter data when you ask them to. See Connect AI Apps with SwiftReporter MCP.
Troubleshooting
- Signature does not match: Confirm you are hashing the raw body, not parsed and re-serialized JSON, and that you are using the secret for this specific endpoint.
- Signature rejected as too old: Check that your server clock is synced (for example, with NTP).
- No events arrive: Confirm the endpoint is enabled, uses
https://, and is subscribed to the event you expect. - Events stop after one attempt: Your endpoint returned a
4xxstatus other than408or429. Fix the handler and trigger a new event. - Duplicate records in your system: Deduplicate on
X-SwiftReporter-Delivery.
