> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pyai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Configure PyAI completion callbacks, verify raw-body signatures, deduplicate deliveries, and run a complete Node.js receiver.

# Receive PyAI completion events

Use webhooks for background completion events. Streaming Hear transcripts and
Omni audio arrive on their live connection; they are not webhook events.

## Choose the event family

| Workflow                 | Configuration                                          | Payload contract                                                                                   |
| ------------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| Async Hear               | `webhook_url` on `POST /v1/transcription/jobs`         | `transcription.job.completed` or `transcription.job.failed`                                        |
| Recap                    | `webhook_url` on `PUT /v1/recap/config`                | `recap.complete` with call identity and record                                                     |
| Integration destinations | Console Integrations or the documented destination API | Event envelope such as `recap.call.completed`; fetch `GET /v1/integrations/events` for the catalog |

These families have different payloads. Read the
[async job guide](https://docs.pyai.com/guides/async-transcription-jobs),
[Recap guide](https://docs.pyai.com/guides/recap-call-intelligence), or
[integration guide](https://docs.pyai.com/guides/integrations-zapier) for the
exact event you are receiving. Do not assume one universal event schema.

## Configure an async job callback

Replace the audio and callback URLs with your own HTTPS URLs:

```sh theme={null}
curl --fail-with-body -sS https://api.pyai.com/v1/transcription/jobs \
  -H "Authorization: Bearer $PYAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: example-1' \
  -d '{"audio_url":"https://media.example.com/recording-481.wav","webhook_url":"https://app.example.com/webhooks/pyai","output_formats":["json"]}'
```

Choose a distinct idempotency key per logical job. For an uncertain response,
retry the same request with the same key. You can also poll
`GET /v1/transcription/jobs/{job_id}` for its status.

## Obtain the signing secret

Use your organization's webhook signing secret, which is separate from the API
key. The authenticated `POST /v1/webhooks/signing-secret` operation mints or
rotates it. Coordinate rotation with your existing receivers; do not rotate a
working organization's secret just to run this example. Store the secret as
`PYAI_WEBHOOK_SECRET` on the receiving server.

## Verify before parsing

PyAI sends the header `X-PyAI-Signature` in this form:

```text theme={null}
t=<unix_seconds>,v1=<hex_digest>
HMAC-SHA256(signing_secret, timestamp + "." + raw_request_body)
```

Read the raw request bytes before JSON middleware changes whitespace or key
ordering. Verify the digest with a constant-time comparison and enforce a
timestamp tolerance. The starter uses five minutes, so keep the receiver clock
synchronized. A recent signature alone does not prevent duplicate delivery.

## Run the receiver

Requires Node.js 22+, `curl`, and `tar`. The archive contains a server, signature
verifier, tests, and `.env.example`. It has no npm dependencies.

```sh theme={null}
curl -fSL https://pyai.com/starters/pyai-webhooks.tar.gz -o pyai-webhooks.tar.gz
tar -xzf pyai-webhooks.tar.gz
cd pyai-webhooks
cp .env.example .env
# Set PYAI_WEBHOOK_SECRET in .env, then:
npm test
npm start
```

Expose port 8080 over HTTPS and set `/webhooks/pyai` as your callback path.
The receiver validates the signature, stores the event in a private local
`inbox/` directory, then returns `204`. It rejects invalid signatures and stale
timestamps. This is a development inbox; use a durable store or queue in
production. Payloads may contain transcripts, so apply your access and retention
policy to stored events.

## Delivery handling

* Acknowledge only after accepting the event into durable storage or a queue.
* Handle the work outside the HTTP request and make side effects idempotent.
* For async jobs, track `X-PyAI-Event-Id` or `Idempotency-Key`; for integration
  envelopes, use the event `id`. Recap consumers should use call identity and
  the documented event semantics. Headers are not covered by the body HMAC;
  also check the signed payload identity before suppressing an event.
* The demo deduplicates identical signed bodies. Production deduplication must
  cover your event family and survive process restarts and multiple replicas.
* Expect retries; retry schedules depend on the product. Use job status APIs
  to reconcile an async result when a callback is missing.

Twilio sends `X-Twilio-Signature`, which uses Twilio's own validation scheme and
auth token. Use the [Twilio starter](https://pyai.com/sdks/twilio.md) for those
requests; do not run them through the PyAI HMAC verifier.

## Troubleshooting

| Symptom                | Check                                                                    |
| ---------------------- | ------------------------------------------------------------------------ |
| Signature rejected     | Correct organization secret, unchanged body bytes, timestamp and clock   |
| Duplicate side effects | Durable, event-specific deduplication before processing                  |
| Missing completion     | Public HTTPS reachability, response status, job status and receiver logs |
| Different event shape  | Async job, Recap and integration contracts are distinct                  |

[Getting started](https://pyai.com/getting-started.md) ·
[API reference](https://api.pyai.com/openapi.json) ·
[Documentation index](https://docs.pyai.com/llms.txt)
