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
- In Swiftner, open Admin → Integrations and enable n8n, Zapier, or Webhooks.
- 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.
- Under Subscriptions, add a row per trigger you want: pick the trigger, paste your endpoint URL, save.
- Use Test connection to send a signed
pingto 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": {}
}
eventis the trigger name (see below). The test probe sends"ping".call_idis Swiftner's call id, ornullonping.external_call_idis the id your system supplied when it created the call via the inbound API, andnullotherwise.useris the sales rep on the call. On stage hooks onlyidis set, andemail/namemay benullif the account was removed.timestampis when the payload was built, in ISO 8601 with a UTC offset.dataholds 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_readyis 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 oncall_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_startedandcall_endedare not retried, since they are point-in-time signals. Anything that has to be reliable should build oncoaching_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.