---
title: "Register the webhook for mid-call questions"
method: PUT
path: "/users/me/webhook"
tags: ["calls"]
---

# Register the webhook for mid-call questions

`PUT /users/me/webhook`

## Request

- Base URL: `https://api.voygr.tech`
- URL: `https://api.voygr.tech/users/me/webhook`
- Auth: API key in header `X-API-Key`

Register the HTTPS endpoint we POST to when the agent needs an answer from you during a call (a hostess asking for a callback number, or an IVR wanting a choice). If you subscribe with `events`, it also receives call_completed when a call finishes. Without one, those questions arrive on GET /calls/{call_id}/events and in pending_questions on GET /calls/{call_id}. While the subscription includes ask_user (the default), the same question goes to both channels carrying the same request_id; answer either, and deduplicate on that id. Reply 200 with an answer field to answer inline, or 202 and then POST /calls/{call_id}/answer when you have it. One 60-second budget covers the POST and the wait after it, and the callee is on hold for all of it.

## Request body

- SetWebhookRequest: Body for PUT /users/me/webhook. Registers the HTTPS endpoint we POST webhooks to (mid-call ask_user questions and, when subscribed, call_completed), and optionally rotates the HMAC secret those POSTs are signed with.
  - `events` string[], nullable: Which webhooks this endpoint receives. Omit to leave the current subscription unchanged; [] receives nothing. 'ask_user' is the mid-call question: without it the webhook gets no questions, which then arrive on GET /calls/{call_id}/events and in pending_questions on GET /calls/{call_id}. 'call_completed' is one event when a call reaches its outcome, signed like ask_user. Its JSON body includes event ('call_completed'), call_id, outcome_type, result, ended_by, line_kind, result_reason, billable, charge_cents, charge_reason, summary, target_phone, started_at, ended_at and at; it carries no transcript or recording (read GET /calls/{call_id}). A failed delivery is retried with the same Webhook-Id, so deduplicate on it. A key that has never set this receives 'ask_user' only: call_completed is opt-in. An unknown value is refused with 422 unknown_webhook_event.
  - `regenerate_secret` boolean: Issue a new HMAC secret, invalidating the previous one. Defaults to true: keeping an old secret alive across a rotation would let anyone who captured it keep signing. Pass false to change only the URL. A key with no secret yet is issued one either way, otherwise the webhook would be unusable.
  - `url` string, uri, required: HTTPS endpoint. Plaintext HTTP is refused, because the payload carries the booking context and the HMAC header. The destination must be publicly routable: no credentials in the URL, no localhost, and nothing the host resolves to may be a private, link-local or otherwise reserved address. Delivery re-checks this and connects to the validated IP, so a host that passes here and later rebinds inward is refused at send time.

## Response `200`

Registered. The secret is present only when this request issued one, and is shown once.

- SetWebhookResponse: PUT /users/me/webhook success body. The secret is returned once, on the response that issues it, and no endpoint returns it again. Store it when you see it.
  - `events` string[]: The subscription now in force, echoed back so you can check it.
  - `secret` string, nullable: The HMAC signing key, present only when this request issued a new one. null means the existing secret was kept, not that there is none. To verify a delivery: the POST carries Webhook-Id, Webhook-Timestamp and Webhook-Signature (Standard Webhooks v1). Recompute HMAC-SHA256 over the Webhook-Id, a dot, the Webhook-Timestamp, a dot, then the raw body bytes. Base64 the digest and compare it against the part after the 'v1,' prefix. The HMAC key is this secret string exactly as returned, UTF-8 encoded, with no whsec_ prefix. Do not base64-decode it first. That differs from the Standard Webhooks whsec_ convention, so a stock Standard Webhooks verifier needs the raw key passed to it, not this string. Sign the bytes you received, never a re-serialization of them: whitespace and key order change an encoding without changing what it means.
  - `url` string, required: The canonicalized URL now registered. May differ from what you sent: the host is lowercased.

## Other responses

- `400`: The URL is not a permitted destination: it is not HTTPS, carries credentials, or resolves to a private or reserved address. Nothing was stored.
- `401`: Missing or invalid X-API-Key.
- `422`: Body failed validation (e.g. url is absent or not a URL), or `events` names an unknown event (unknown_webhook_event).

## Changes

- **2026-10-30** `7199881ed19e`: 2 breaking, 1 warning
  - the `url` request property's minLength was increased from `0` to `1`
  - response property `secret` list-of-types was widened by adding types `null` to media type `application/json` of response `200`
  - the `url` request property's maxLength was set to `2000`
- **2026-10-15** `2f4d9658b334`: 2 info
  - added the new optional request property `events`
  - added the optional property `events` to the response with the `200` status
- **2026-10-02** `50f6b143949e`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/voygr/apis/voygr-calls-api/changes/users/me/webhook/put.md)

---

[API](https://skmtc.dev/voygr/apis/voygr-calls-api.md) - [All operations](https://skmtc.dev/voygr/apis/voygr-calls-api/llms.txt) - [OpenAPI document](https://skmtc.dev/voygr/apis/voygr-calls-api/revisions/7199881ed19e?raw)
