---
title: "Webhooks"
nav_title: "Webhooks"
description: "Subscribe to conversation events with webhook delivery, custom auth headers, idempotency keys, and automatic retries."
tags: ["webhooks", "event-driven automation", "CRM integration", "real-time notifications"]
date: "2026-05-08"
nav_order: 3
nav_display: true
---

# Webhooks

Webhooks send event data to your server when a Perspective conversation reaches a terminal state. Use them to sync data into your CRM, trigger Slack alerts, or start downstream workflows.

## Supported Events

| Event | Trigger |
|-------|---------|
| `interview.completed` | A participant completes a conversation. |
| `interview.partial` | A conversation reaches a partial-completion state (the participant left mid-conversation but enough was captured to be useful). |
| `interview.abandoned` | A conversation is abandoned without a usable result. |

Events are delivered as `POST` requests to the endpoint you configure on a webhook automation.

## Setting Up a Webhook

You'll need an HTTPS endpoint that accepts `POST` requests and responds with a `2xx` status code.

To configure with the assistant, open the Perspective's Design page and ask it to set up a webhook (e.g. *"set up a webhook on this perspective"*). If you are in Results, open the Perspective actions menu and choose **Edit** to get back to Design. A modal will open. Enter your endpoint URL (HTTPS only) and, optionally, an authorization header (name + value) that Perspective will include with every request. Your endpoint can use this header to authenticate incoming deliveries. Save, and the automation starts firing on the next completed, partial, or abandoned conversation (enabled by default).

If you'd rather find it through the menus: in the editor side panel, switch to **Settings**, expand **Automations**, and click **Add** on the **Per Interview** tab. The chat takes over from there. For broader automation patterns, see [Automation Recipes](/docs/build/automation-recipes).

## Payload

Every delivery includes a JSON body with this structure:

```json
{
  "event": "interview.completed",
  "status": "COMPLETE",
  "returned_after_partial_conversation": false,
  "test": false,
  "timestamp": "2026-03-30T14:22:00Z",
  "research_id": "research_xyz",
  "interview_id": "interview_abc",
  "participant_metadata": {
    "utm_source": "newsletter",
    "plan": "pro"
  },
  "structured_output": {
    "summary": "...",
    "rating": 8
  }
}
```

| Field | Type | Notes |
|-------|------|-------|
| `event` | string | `interview.completed`, `interview.partial`, or `interview.abandoned`. |
| `status` | string | `COMPLETE`, `PARTIAL`, or `ABANDONED`. |
| `returned_after_partial_conversation` | boolean | `true` if the participant resumed a previously-partial conversation and completed it. Always `false` for non-`COMPLETE` events. |
| `test` | boolean | `false` for real conversation deliveries; `true` for test deliveries sent from automation test flows. |
| `timestamp` | string | ISO 8601 timestamp at which the payload was generated. |
| `research_id` | string | The perspective (research) ID. |
| `interview_id` | string | The conversation ID. |
| `participant_metadata` | object \| null | Flat key/value map of all metadata captured for the participant: URL params, embed attributes, invite context, and platform-derived keys (e.g. from Slack) merged into one object. `null` if no metadata was captured. |
| `structured_output` | object \| null | The values the conversation captured, keyed by the form fields defined on the perspective. These are the same values shown under the **Form Data** tab when you open a completed conversation. To see which fields a perspective captures, go to its **Edit** page, switch to the **Outline** view, and check the **Form** section in the **Data Capture** tab. Each field lists its key, type (text, number, yes/no, file, or list), and description. Usually `null` for `interview.abandoned` and for perspectives with no form fields defined. |

## Headers and Authentication

Perspective sends each delivery with:

- `Content-Type: application/json`
- Your configured authorization header (if set), exactly as `name: value` on the request.
- `X-Idempotency-Key` -- a stable identifier for real workflow deliveries when one is available. The same key is reused across retries of the same delivery, so you can deduplicate by storing processed keys. Test deliveries sent from the in-app test feature don't include this header.

Perspective does not sign webhook bodies today. Use HTTPS, your custom authorization header, and idempotency-key deduplication to authenticate and safely process deliveries.

If you configured an authorization header, verify it on every request before processing the body and reject any request that doesn't include a valid value.

## Retry Behavior

Perspective retries failed deliveries automatically.

- **4xx responses** are treated as permanent (e.g. configuration or auth errors) and not retried.
- **5xx responses and network errors** are treated as transient and retried.
- Each attempt has a **30-second request timeout**; a slower response counts as a transient failure and is retried.

Return `2xx` as quickly as possible. Offload heavy processing to a background job on your side.

## Filtering Deliveries

Ask the assistant to add filters so the webhook only fires for certain conversations:

- **By tag**: fire only when the conversation has at least one of the specified tags, or only when it has all of them.
- **By trust score**: fire only when the conversation's trust score falls within a minimum and/or maximum (inclusive). Conversations without a trust score (e.g. abandoned ones) won't pass a trust-score filter, so they're effectively excluded.

Tags are assigned automatically based on the conversation's content. You don't define them yourself. The set of available tags depends on the perspective's type; ask the assistant *"which tags are available for this perspective?"* and you'll get the list with descriptions of when each one applies.

When multiple filters are set on the same automation, all of them must pass for a delivery to fire.

## Common Pitfalls & Fixes

### Webhook never fires after a conversation completes

Open the Perspective's Design page. In the right-side panel, switch to **Settings** and expand **Automations**. Confirm the automation is enabled and is not showing an error. Verify any tag or trust-score filters actually match the conversation. To check delivery without waiting for a real conversation, ask the assistant to run a test of the automation. Also make sure your endpoint is reachable from the public internet; `localhost` URLs won't work.

### Automation is marked as failing

A failing automation shows its last error message and consecutive-failure count in the Automations list. The chat panel also displays a banner with a **Fix it** button that opens the assistant pre-loaded to help. Common causes: endpoint URL is unreachable, HTTPS certificate issue, or your server returning a 4xx response (4xx is treated as permanent, so Perspective stops retrying). Fix the underlying issue, then ask the assistant to test the automation again to clear the error state.

### Receiving duplicate deliveries

Use the `X-Idempotency-Key` header to deduplicate. Retries reuse the same key, so store processed keys on your side and skip duplicates.

### Authorization header rejected by your server

The header name and value you configured are sent verbatim. Your server should compare against that exact pair (not, for example, an `Authorization: Bearer <token>` form unless that's exactly what you set). To change the value, open the Perspective's Design page, switch the right-side panel to **Settings**, expand **Automations**, click the pencil icon on the webhook automation, and edit the URL or auth header in the modal that appears.

### Your endpoint takes too long to respond

Each delivery has a 30-second timeout. If your handler exceeds it, the request is aborted and retried as a transient failure. Acknowledge with `2xx` immediately and process heavy work asynchronously.
