Webhooks: events, payloads and signatures

Webhooks let your own systems react the moment Inwista finishes work: when a transcription completes or a video export is rendered, Inwista POSTs a signed JSON event to an HTTPS endpoint you control. Use it to feed publishing pipelines, archive systems, project trackers — anything that should happen automatically after Inwista is done.

Setup and the signing secret

In My workspaceIntegrations, connect the Webhooks card and enter your endpoint URL (HTTPS required). Inwista generates a signing secret with the prefix whsec_ and shows it exactly once — store it in your secret manager right away. You can rotate the secret at any time from the integration settings; the old secret stops working immediately.

Use Send test event in the settings to fire a signed webhook.test event at your endpoint and confirm the wiring before any real traffic. Changing the endpoint URL later keeps the same secret.

Events

  • transcript.completed Fired once per project, when the initial transcription finishes. Carries download links for the transcript files in the formats configured on the integration.
  • export.completed Fired every time a video render finishes. Carries the rendered video plus fresh transcript files reflecting any subtitle edits made since transcription.
  • webhook.test Fired manually from the integration settings, for verifying your receiver.

Both event types can be toggled independently in the integration settings. Events fire on every subscribed occurrence regardless of per-export storage selections — webhooks are notifications, not delivery destinations.

Payload

Every event shares the same envelope: an event type, a unique id for deduplication, a Unix timestamp, the workspaceId, and a project block. File-bearing events add a files array; export.completed also includes a video object.

{
  "event": "export.completed",
  "id": "evt_9c1b7e2a4f0d4b6e8a12",
  "timestamp": 1784034017,
  "workspaceId": "UK6naH1L1sMtzTVcVZih",
  "project": {
    "id": "abc123def456",
    "name": "Interview — Episode 12",
    "language": "no",
    "durationSeconds": 1834
  },
  "files": [
    {
      "format": "srt",
      "name": "Interview — Episode 12.srt",
      "mimeType": "application/x-subrip",
      "url": "https://storage.googleapis.com/...signed...",
      "expiresAt": 1784120417
    }
  ],
  "video": {
    "name": "Interview — Episode 12.mp4",
    "mimeType": "video/mp4",
    "sizeBytes": 812340221,
    "url": "https://storage.googleapis.com/...signed...",
    "expiresAt": 1784120417
  }
}

The url values are signed download links valid for 24 hours (the expiresAt Unix timestamp says exactly when) — fetch what you need promptly rather than storing the links. transcript.completed has the same shape without the video object.

Verifying signatures

Every request carries three headers: X-Inwista-Event (the event type), X-Inwista-Delivery (the event id) and X-Inwista-Signature — an HMAC-SHA256 of the raw request body, computed with your signing secret. Recompute it and compare before trusting the payload; without this check, anyone who discovers your endpoint URL could feed you fake events.

const crypto = require("crypto");

function verifyInwistaSignature(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    signatureHeader.length === expected.length &&
    crypto.timingSafeEqual(
      Buffer.from(signatureHeader),
      Buffer.from(expected),
    )
  );
}

// Express example — capture the raw body for verification:
// app.post("/inwista-hook", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyInwistaSignature(req.body, req.get("X-Inwista-Signature"), process.env.INWISTA_WEBHOOK_SECRET)) {
//     return res.status(401).end();
//   }
//   const event = JSON.parse(req.body);
//   res.status(200).end(); // acknowledge fast, process async
// });

Important: compute the HMAC over the raw request body, not a re-serialized version of the parsed JSON — key ordering and whitespace differences will break the comparison.

Delivery, retries and deduplication

  • Acknowledge with a 2xx Respond within 10 seconds. Do heavy processing asynchronously after acknowledging — a slow response counts as a failure.
  • Retries Failed deliveries are retried up to two more times (roughly 10 and 30 seconds later). Responses in the 4xx range are treated as permanent and not retried, except 408 and 429.
  • Deduplicate by id If your server accepts a request but the response is lost, a retry can deliver the same event twice. The id field is stable across retries — use it to ignore duplicates.
  • Automatic pause After 10 consecutive failed deliveries, Inwista pauses the endpoint and shows why in the integration settings, where you can resume it once your receiver is fixed. The last delivery's status and HTTP code are always visible there.