Skip to main content

Help center / Webhooks & API

Webhooks & API

Integrate Swiftner with n8n, Zapier, or your own systems using signed webhooks and the inbound automation API.

Swiftner can notify your systems the moment something happens on a call, and it lets your systems talk back. Three integration tiles use this contract: n8n, Zapier, and generic Webhooks. They all send the same payloads; only the receiving tool differs.

There are two directions:

  • Outbound (webhooks): Swiftner POSTs signed JSON to URLs you configure. It fires when a call starts or ends, when a coaching report is ready, or at hook points during call processing (stage hooks).
  • Inbound (API): your systems push data into Swiftner with your API key. You can start calls, attach caller identity, submit recordings, and fetch reports.

Getting started

  1. In Swiftner, open Admin → Integrations and enable n8n, Zapier, or Webhooks.
  2. In the integration drawer, generate your credentials: an API key (for inbound calls) and a signing secret (to verify outbound webhooks). Copy both now, because they are shown only once.
  3. Under Subscriptions, add a row per trigger you want: pick the trigger, paste your endpoint URL, save.
  4. Use Test connection to send a signed ping to every configured URL.

The envelope

Events, stage hooks, and the test ping all use the same JSON shape:

{
  "event": "call_ended",
  "call_id": "8f14e45f-ceea-467f-a8d6-9f6d3d7f9f10",
  "external_call_id": "your-systems-call-id-or-null",
  "user": {
    "id": "d3b07384-d9a0-4c9e-8b6e-1a2b3c4d5e6f",
    "email": "rep@example.com",
    "name": "Rep Name"
  },
  "workspace_id": "b6589fc6-ab0d-4c82-8b0e-0242ac130003",
  "timestamp": "2026-07-16T17:22:05.123456+00:00",
  "data": {}
}
  • event is the trigger name (see below). The test probe sends "ping".
  • call_id is Swiftner's call id, or null on ping.
  • external_call_id is the id your system supplied when it created the call via the inbound API, and null otherwise.
  • user is the sales rep on the call. On stage hooks only id is set, and email/name may be null if the account was removed.
  • timestamp is when the payload was built, in ISO 8601 with a UTC offset.
  • data holds the event-specific fields documented per event below.

Event subscriptions

call_started fires when a rep's live session opens for a call. Use it for screen-pops, or to kick off a workflow for each call.

"data": {
  "identifiers": {
    "phone_number": "+4791234567",
    "org_number": "912345678"
  }
}

identifiers contains whatever Swiftner has learned about the caller from its integrations at that moment. Either key may be absent, and the object may be empty. Calls that your own flow created via the inbound API do not echo a call_started back to you.

call_ended fires when the call ends, before AI processing completes.

"data": { "duration_seconds": 342 }

coaching_report_ready fires when post-call AI analysis is complete. This is the payload to write back to your CRM.

"data": {
  "call_summary": "…",
  "coaching_summary": "…",
  "focus_area": "…",
  "coaching_moments": ["…", "…"],
  "interest_level": "…",
  "interest_reason": "…"
}

Any field may be null, for example on calls too short to analyze.

Stage hooks

Stage hooks are request-response. Swiftner POSTs a snapshot at a fixed point in call processing, and if your endpoint responds with a JSON object, those fields are saved onto the call. They then show up in the dashboard's call view, and Swiftner's AI can use them as context. This is how your flow injects CRM data into live coaching, or records a write-back confirmation on the call.

Trigger When it fires
stage:identify Call start, while caller identity is being resolved
stage:context After identify, during history lookup
stage:enrich After context, during external data enrichment
stage:prepare Before coaching starts, once talking points are assembled
stage:summarize After the call, before AI summaries are generated
stage:export After the call is fully processed, to push results out

Request data:

"data": {
  "stage": "enrich",
  "identifiers": { "phone_number": "+4791234567", "org_number": null },
  "enrichment_context": { "…": "everything integrations have attached to this call so far" }
}

Response contract:

  • Respond within the deadline: 3 seconds by default, 10 seconds maximum (configurable in the integration's settings).
  • A JSON object body is merged into the call's context under your integration's namespace. Later subscriptions on the same stage merge over earlier ones, key by key.
  • Anything else (empty body, non-object JSON, non-2xx) is ignored. A failing stage hook never breaks call processing.

Verifying signatures

Every outbound request carries:

X-Swiftner-Signature: t=1784309054,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

t is a Unix timestamp (seconds); v1 is HMAC-SHA256(signing_secret, "{t}." + raw_body) in hex. Verify before trusting any payload.

Node.js

const crypto = require("crypto");

function verifySwiftnerSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.`)
    .update(rawBody) // the raw request bytes, before JSON parsing
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Python

import hashlib, hmac, time

def verify_swiftner_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    if "t" not in parts or "v1" not in parts:
        return False
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Compute the HMAC over the raw request body. Parsing and re-serializing the JSON changes the bytes and fails verification. Reject stale timestamps to block replays; 5 minutes is a sensible tolerance. In n8n and Zapier, drop these snippets into a Code or Function step before acting on the payload.

Delivery and retries

  • Respond with any 2xx quickly; for event subscriptions the response body is ignored.
  • Event webhook timeout: 10 seconds.
  • coaching_report_ready is retried on failure (timeouts, 429, 5xx): up to 3 attempts, roughly 15 minutes apart. Your endpoint may therefore see the same report more than once, so deduplicate on call_id + event. If you have several subscriptions on the same trigger, a retry re-delivers to all of them, including ones that already succeeded.
  • call_started and call_ended are not retried, since they are point-in-time signals. Anything that has to be reliable should build on coaching_report_ready, or on polling the inbound API.
  • Other 4xx responses are treated as rejections and never retried.

Inbound API

Everything inbound authenticates with your API key in the X-API-Key header. Base URL: https://api.hud.swiftner.app/api/v1/automation.

Endpoint What it does
GET /me Verify your key; returns your integration + tenant
PUT /external_users Map your system's users to Swiftner reps
PUT /external_groups Map your teams/groups to Swiftner workspaces
POST /calls/start Announce a call (idempotent on external_call_id)
POST /calls/{id}/identify Attach caller phone / org number
POST /calls/{id}/end Mark the call ended
POST /calls/{id}/context Push free-form context onto the call (max 32 KB)
POST /calls/{id}/recording Submit a recording URL for transcription (once per call)
GET /calls, GET /calls/{id} List / read calls
GET /calls/{id}/report Fetch the coaching report

A typical flow: your dialer rings → POST /calls/start with your external_call_id → Swiftner enriches and, when the rep's session opens, coaching begins → your call_ended/coaching_report_ready subscriptions fire → your flow writes the summary back to your CRM.