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 accepts POST requests 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

EventWhen it is sent
inspection.completedAn inspection's status changes to Complete.
invoice.paidAn invoice's status changes to Paid.
booking.createdA 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:

HeaderValue
Content-Typeapplication/json
User-AgentSwiftReporter-Webhooks/1.0
X-SwiftReporter-EventThe event type, such as invoice.paid.
X-SwiftReporter-DeliveryThe event ID. Matches the id field in the body.
X-SwiftReporter-Signaturet=<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.

  1. 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.
  2. Split the X-SwiftReporter-Signature header on , and read the t (timestamp) and v1 (signature) values.
  3. Reject the request if t is more than 300 seconds (5 minutes) away from your server's current time. This blocks replayed requests.
  4. Build the signed content as ${t}.${rawBody}, the timestamp, a period, then the raw body.
  5. Compute HMAC_SHA256(signing_secret, signed_content) as lowercase hex.
  6. Compare your result with v1 using 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 2xx status 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:

AttemptWhen it is sent
1Immediately
2About 1 second after attempt 1
3About 4 seconds after attempt 2
4About 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:

  1. Before processing, check whether you have already stored this delivery ID.
  2. If you have, return 200 and skip processing.
  3. 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:

  1. In Zapier, create a Zap and choose Webhooks by Zapier → Catch Hook as the trigger.
  2. Copy the webhook URL Zapier gives you.
  3. In SwiftReporter, open Account → Webhooks, add that URL as an endpoint, and select the events you want.
  4. Trigger a test event, such as completing a test inspection, so Zapier can read a sample payload.
  5. Map fields from data into 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 4xx status other than 408 or 429. Fix the handler and trigger a new event.
  • Duplicate records in your system: Deduplicate on X-SwiftReporter-Delivery.

Related