# Webhook events

Each event is an HTTPS POST to your endpoint, in the same envelope with its own data. How to answer a delivery, check its signature and read its retries is on the endpoints page (https://vocatech.com/developers/reference/webhooks).

## call.started

An extension started ringing or dialing.

An extension started ringing on an incoming call, or started dialing an outgoing one. `end_time` and `duration` are null.

We look for new call events every 5 seconds, so it usually arrives within seconds. Each event is one extension's view of the call: a call that rings several extensions can send one event per extension. Numbers are digits without the leading 1.

Known issue: `group_number` is one fixed number of your account, the same on every call event, not always the number that was dialed. A `number_filters` entry for your own number can miss these events.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `call.started`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.call_id` | string | yes | The phone system's id for the call. It ties a call's events together. It is not always the same as `call_id` in `GET /v1/calls`. |
| `data.direction` | string | yes | `incoming`, `outgoing`, or `unknown` when the phone system did not say. One of `incoming`, `outgoing` or `unknown`. |
| `data.extension` | string or null | yes | The extension this event is about. |
| `data.extension_name` | string or null | yes | Its name, written First Last. |
| `data.remote_name` | string or null | yes | The other party's name from caller ID. |
| `data.remote_number` | string or null | yes | The other party's number, digits without the leading 1. |
| `data.group_number` | string or null | yes | One number of your account, digits without the leading 1. Known issue: it is the same number on every call event, not always the number that was dialed. |
| `data.start_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `data.end_time` | string (date-time) or null | yes | Null until the call ends. |
| `data.duration` | integer or null | yes | Seconds from answer to hang-up. Null until the call ends, and on a call nobody answered. |

**Example delivery**

```json
{
  "id": "evt_6703a1f25c9e8.21736450",
  "event_type": "call.started",
  "timestamp": "2026-09-24T14:03:14+00:00",
  "company_id": 1234567890,
  "data": {
    "call_id": "8a1f3c2e-5b7d-4e9a-9c1b-2d4e6f8a0b1c",
    "direction": "incoming",
    "extension": "104",
    "extension_name": "Maya Chen",
    "remote_name": "Jordan Reyes",
    "remote_number": "7185550142",
    "group_number": "7185550100",
    "start_time": "2026-09-24T14:03:12.000Z",
    "end_time": null,
    "duration": null
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## call.answered

An extension answered.

An extension answered the call. `end_time` and `duration` are null.

We look for new call events every 5 seconds, so it usually arrives within seconds. Each event is one extension's view of the call: a call that rings several extensions can send one event per extension. Numbers are digits without the leading 1.

Known issue: `group_number` is one fixed number of your account, the same on every call event, not always the number that was dialed. A `number_filters` entry for your own number can miss these events.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `call.answered`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.call_id` | string | yes | The phone system's id for the call. It ties a call's events together. It is not always the same as `call_id` in `GET /v1/calls`. |
| `data.direction` | string | yes | `incoming`, `outgoing`, or `unknown` when the phone system did not say. One of `incoming`, `outgoing` or `unknown`. |
| `data.extension` | string or null | yes | The extension this event is about. |
| `data.extension_name` | string or null | yes | Its name, written First Last. |
| `data.remote_name` | string or null | yes | The other party's name from caller ID. |
| `data.remote_number` | string or null | yes | The other party's number, digits without the leading 1. |
| `data.group_number` | string or null | yes | One number of your account, digits without the leading 1. Known issue: it is the same number on every call event, not always the number that was dialed. |
| `data.start_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `data.end_time` | string (date-time) or null | yes | Null until the call ends. |
| `data.duration` | integer or null | yes | Seconds from answer to hang-up. Null until the call ends, and on a call nobody answered. |

**Example delivery**

```json
{
  "id": "evt_6703a1f3d7b21.58214907",
  "event_type": "call.answered",
  "timestamp": "2026-09-24T14:03:33+00:00",
  "company_id": 1234567890,
  "data": {
    "call_id": "8a1f3c2e-5b7d-4e9a-9c1b-2d4e6f8a0b1c",
    "direction": "incoming",
    "extension": "104",
    "extension_name": "Maya Chen",
    "remote_name": "Jordan Reyes",
    "remote_number": "7185550142",
    "group_number": "7185550100",
    "start_time": "2026-09-24T14:03:12.000Z",
    "end_time": null,
    "duration": null
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## call.ended

A call ended.

The call ended for an extension. `duration` is seconds from answer to hang-up, or null when nobody answered.

We look for new call events every 5 seconds, so it usually arrives within seconds. Each event is one extension's view of the call: a call that rings several extensions can send one event per extension. Numbers are digits without the leading 1.

Known issue: `group_number` is one fixed number of your account, the same on every call event, not always the number that was dialed. A `number_filters` entry for your own number can miss these events.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `call.ended`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.call_id` | string | yes | The phone system's id for the call. It ties a call's events together. It is not always the same as `call_id` in `GET /v1/calls`. |
| `data.direction` | string | yes | `incoming`, `outgoing`, or `unknown` when the phone system did not say. One of `incoming`, `outgoing` or `unknown`. |
| `data.extension` | string or null | yes | The extension this event is about. |
| `data.extension_name` | string or null | yes | Its name, written First Last. |
| `data.remote_name` | string or null | yes | The other party's name from caller ID. |
| `data.remote_number` | string or null | yes | The other party's number, digits without the leading 1. |
| `data.group_number` | string or null | yes | One number of your account, digits without the leading 1. Known issue: it is the same number on every call event, not always the number that was dialed. |
| `data.start_time` | string (date-time) or null | yes | UTC, ISO 8601 with milliseconds. |
| `data.end_time` | string (date-time) or null | yes | Null until the call ends. |
| `data.duration` | integer or null | yes | Seconds from answer to hang-up. Null until the call ends, and on a call nobody answered. |

**Example delivery**

```json
{
  "id": "evt_6703a1f8e0c46.73920518",
  "event_type": "call.ended",
  "timestamp": "2026-09-24T14:09:43+00:00",
  "company_id": 1234567890,
  "data": {
    "call_id": "8a1f3c2e-5b7d-4e9a-9c1b-2d4e6f8a0b1c",
    "direction": "incoming",
    "extension": "104",
    "extension_name": "Maya Chen",
    "remote_name": "Jordan Reyes",
    "remote_number": "7185550142",
    "group_number": "7185550100",
    "start_time": "2026-09-24T14:03:12.000Z",
    "end_time": "2026-09-24T14:09:40.000Z",
    "duration": 369
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## call.transcription

A recorded call has its transcript.

A recorded call leg has its transcript and AI summary. One event per recording: a call parked and picked up again is recorded in parts and sends one event per part.

It is sent once the finished transcript is ready, usually a few minutes after the call. We check every 2 minutes and wait up to 6 hours for the finished transcript. `extension` is the recorded leg's extension. The times and `duration` are the whole call's. `direction` reads as in `GET /v1/calls`.

`recording_id`, `part`, `parts`, `recording_start_time` and `recording_duration` say which recording the words are. Part N of a leg is `GET /v1/media/rec_<n>?part=N` and the same entry of `recordings` in `GET /v1/calls`. `parts` counts the parts saved when the event was sent. `part` is null for a recording of another call leg that is linked to the same leg of the report.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `call.transcription`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.call_id` | string | yes | The phone system's id for the call. |
| `data.direction` | string | yes | `incoming` or `outgoing`, as `GET /v1/calls` shows it. |
| `data.extension` | string or null | yes | The extension on the recorded leg. |
| `data.extension_name` | string or null | yes |  |
| `data.remote_name` | string or null | yes |  |
| `data.remote_number` | string or null | yes | Digits without the leading 1. |
| `data.group_number` | string or null | yes | Your number on the call, digits without the leading 1. |
| `data.start_time` | string (date-time) or null | yes | The call's start. UTC, ISO 8601 with milliseconds. |
| `data.end_time` | string (date-time) or null | yes | The call's end. UTC, ISO 8601 with milliseconds. |
| `data.duration` | integer | yes | The call's length in seconds. |
| `data.summary` | string or null | yes | The AI summary. Null when there is none. |
| `data.transcription` | string | yes | The transcript, with speaker labels. |
| `data.recording_id` | integer |  | The recording these words are, as in `recordings` in `GET /v1/calls`. |
| `data.part` | integer or null |  | This recording's part of the leg, from 1. Null for a recording of another call leg linked to the same leg of the report. |
| `data.parts` | integer or null |  | How many parts the leg's recording had when the event was sent. Nearly always 1. |
| `data.recording_start_time` | string (date-time) or null |  | When this recording started. UTC, ISO 8601 with milliseconds. |
| `data.recording_duration` | integer or null |  | This recording's length in seconds. |

**Example delivery**

```json
{
  "id": "evt_6703a2b41f9d2.30481726",
  "event_type": "call.transcription",
  "timestamp": "2026-09-24T14:13:05+00:00",
  "company_id": 1234567890,
  "data": {
    "call_id": "8a1f3c2e-5b7d-4e9a-9c1b-2d4e6f8a0b1c",
    "direction": "incoming",
    "extension": "104",
    "extension_name": "Maya Chen",
    "remote_name": "Jordan Reyes",
    "remote_number": "7185550142",
    "group_number": "7185550100",
    "start_time": "2026-09-24T14:03:12.000Z",
    "end_time": "2026-09-24T14:09:40.000Z",
    "duration": 388,
    "summary": "Jordan Reyes asked to move the Thursday appointment to Friday at 10 AM. Maya Chen confirmed the new time and said a reminder text will follow.",
    "transcription": "Speaker 1: Good morning, thanks for calling. This is Maya.\nSpeaker 2: Hi Maya, this is Jordan Reyes. I need to move my Thursday appointment.\nSpeaker 1: No problem. Would Friday at 10 AM work?\nSpeaker 2: Friday at 10 is perfect. Thank you.",
    "recording_id": 10577702,
    "part": 2,
    "parts": 2,
    "recording_start_time": "2026-09-24T14:04:31.000Z",
    "recording_duration": 309
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## message.sent

A text went out through the API or an automation.

A text left through the API or an automation: a send on a number in `api` or `email` mode, a picture message sent through the API, or a mass or group text. It is sent as soon as the carrier takes the message. `message_id` is the carrier's id, the same one the send returned. For a picture message sent through the API, `attachments` lists the URLs you sent.

Today a text sent from Webex does not fire `message.sent`. That includes `POST /v1/messages` on a Webex-mode number, replies your team types in Webex or sends by email, and WhatsApp replies. Those messages are in `GET /v1/messages`.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `message.sent`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.message_id` | string or null | yes | For a text, the carrier's message id: the same one a send in `api` or `email` mode returns and `GET /v1/messages` shows. For WhatsApp, the WhatsApp message id. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.channel` | string | yes | One of `text` or `whatsapp`. |
| `data.from` | string | yes | E.164, like `+17185550142`. |
| `data.to` | string | yes | E.164. On a group text, the recipients. |
| `data.body` | string | yes | The message text. Empty on most delivery receipts. |
| `data.status` | string | yes | `delivered` on received messages, `sent` on sent ones. On status updates: for a text `delivered` or `failed`; for WhatsApp its status, for example `delivered`, `read` or `failed`. |
| `data.error` | string or null | yes | Why a message failed, from the carrier. Null otherwise. |
| `data.sent_at` | string or null | yes | When the message was sent or received, ISO 8601 as the source reported it. The offset varies. |
| `data.attachments` | array, each object or string (uri) | yes | Files on the message. Received messages list objects. A picture message sent through the API lists the URLs you sent. |
| `data.attachments[].url` | string (uri) or null | yes | `https://api.vocatech.com/v1/media/att_<n>`: call it with your key for a signed link. |
| `data.attachments[].content_type` | string or null | yes |  |
| `data.attachments[].filename` | string or null | yes |  |
| `data.attachments[].size` | integer or null | yes |  |

**Example delivery**

```json
{
  "id": "evt_6703a3d19b6f2.17394056",
  "event_type": "message.sent",
  "timestamp": "2026-09-24T14:22:06+00:00",
  "company_id": 1234567890,
  "data": {
    "message_id": "msg_9b2e5a",
    "direction": "outgoing",
    "channel": "text",
    "from": "+17185550100",
    "to": "+17185550142",
    "body": "Friday at 10 AM works. See you then.",
    "status": "sent",
    "error": null,
    "sent_at": "2026-09-24T10:22:05-04:00",
    "attachments": []
  }
}
```

**Example delivery**

```json
{
  "id": "evt_6703a3e2b0d58.47213960",
  "event_type": "message.sent",
  "timestamp": "2026-09-24T14:06:45+00:00",
  "company_id": 1234567890,
  "data": {
    "message_id": "msg_5d8f1b",
    "direction": "outgoing",
    "channel": "text",
    "from": "+17185550100",
    "to": "+17185550142",
    "body": "Here is the estimate we discussed.",
    "status": "sent",
    "error": null,
    "sent_at": "2026-09-24T10:06:44-04:00",
    "attachments": [
      "https://files.example.com/estimates/estimate-2291.pdf"
    ]
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## message.received

A text or WhatsApp message arrived.

A text or WhatsApp message arrived on one of your numbers. It is sent as soon as the message arrives.

`attachments` lists each picture or file with its link to `GET /v1/media`. A file we could not store has `url` set to null.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `message.received`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.message_id` | string or null | yes | For a text, the carrier's message id: the same one a send in `api` or `email` mode returns and `GET /v1/messages` shows. For WhatsApp, the WhatsApp message id. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.channel` | string | yes | One of `text` or `whatsapp`. |
| `data.from` | string | yes | E.164, like `+17185550142`. |
| `data.to` | string | yes | E.164. On a group text, the recipients. |
| `data.body` | string | yes | The message text. Empty on most delivery receipts. |
| `data.status` | string | yes | `delivered` on received messages, `sent` on sent ones. On status updates: for a text `delivered` or `failed`; for WhatsApp its status, for example `delivered`, `read` or `failed`. |
| `data.error` | string or null | yes | Why a message failed, from the carrier. Null otherwise. |
| `data.sent_at` | string or null | yes | When the message was sent or received, ISO 8601 as the source reported it. The offset varies. |
| `data.attachments` | array, each object or string (uri) | yes | Files on the message. Received messages list objects. A picture message sent through the API lists the URLs you sent. |
| `data.attachments[].url` | string (uri) or null | yes | `https://api.vocatech.com/v1/media/att_<n>`: call it with your key for a signed link. |
| `data.attachments[].content_type` | string or null | yes |  |
| `data.attachments[].filename` | string or null | yes |  |
| `data.attachments[].size` | integer or null | yes |  |

**Example delivery**

```json
{
  "id": "evt_6703a3c07a5e1.64820193",
  "event_type": "message.received",
  "timestamp": "2026-09-24T14:20:32+00:00",
  "company_id": 1234567890,
  "data": {
    "message_id": "msg_7f3a9c",
    "direction": "incoming",
    "channel": "text",
    "from": "+17185550142",
    "to": "+17185550100",
    "body": "Can we move my appointment to Friday?",
    "status": "delivered",
    "error": null,
    "sent_at": "2026-09-24T14:20:31.512Z",
    "attachments": [
      {
        "url": "https://api.vocatech.com/v1/media/att_90211",
        "content_type": "image/jpeg",
        "filename": "photo-0924.jpg",
        "size": 184233
      }
    ]
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## message.status_updated

The carrier reported on a text.

The carrier reported what happened to a text you sent, or WhatsApp reported a status change. It is sent as soon as the report arrives.

For a text, `status` is `delivered`, or `failed` with the carrier's reason in `error`. A text whose receipt never came back sends no event. For WhatsApp, `status` is the WhatsApp status, for example `delivered`, `read` or `failed`. `body` is empty except on a failed text. Match `message_id` to the send or to `GET /v1/messages`.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `message.status_updated`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.message_id` | string or null | yes | For a text, the carrier's message id: the same one a send in `api` or `email` mode returns and `GET /v1/messages` shows. For WhatsApp, the WhatsApp message id. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.channel` | string | yes | One of `text` or `whatsapp`. |
| `data.from` | string | yes | E.164, like `+17185550142`. |
| `data.to` | string | yes | E.164. On a group text, the recipients. |
| `data.body` | string | yes | The message text. Empty on most delivery receipts. |
| `data.status` | string | yes | `delivered` on received messages, `sent` on sent ones. On status updates: for a text `delivered` or `failed`; for WhatsApp its status, for example `delivered`, `read` or `failed`. |
| `data.error` | string or null | yes | Why a message failed, from the carrier. Null otherwise. |
| `data.sent_at` | string or null | yes | When the message was sent or received, ISO 8601 as the source reported it. The offset varies. |
| `data.attachments` | array, each object or string (uri) | yes | Files on the message. Received messages list objects. A picture message sent through the API lists the URLs you sent. |
| `data.attachments[].url` | string (uri) or null | yes | `https://api.vocatech.com/v1/media/att_<n>`: call it with your key for a signed link. |
| `data.attachments[].content_type` | string or null | yes |  |
| `data.attachments[].filename` | string or null | yes |  |
| `data.attachments[].size` | integer or null | yes |  |

**Example delivery**

```json
{
  "id": "evt_6703a3d4c1a07.92057341",
  "event_type": "message.status_updated",
  "timestamp": "2026-09-24T14:22:09+00:00",
  "company_id": 1234567890,
  "data": {
    "message_id": "msg_9b2e5a",
    "direction": "outgoing",
    "channel": "text",
    "from": "+17185550100",
    "to": "+17185550142",
    "body": "",
    "status": "delivered",
    "error": null,
    "sent_at": "2026-09-24T14:22:07.880Z",
    "attachments": []
  }
}
```

**Example delivery**

```json
{
  "id": "evt_6703a3d6e8b14.30958216",
  "event_type": "message.status_updated",
  "timestamp": "2026-09-24T14:25:40+00:00",
  "company_id": 1234567890,
  "data": {
    "message_id": "msg_1e7b4d",
    "direction": "outgoing",
    "channel": "text",
    "from": "+17185550100",
    "to": "+17185550143",
    "body": "Friday at 10 AM works. See you then.",
    "status": "failed",
    "error": "Carrier rejected the message",
    "sent_at": "2026-09-24T14:25:38.214Z",
    "attachments": []
  }
}
```

**Example delivery**

```json
{
  "id": "evt_6703a3d9a2c75.11846230",
  "event_type": "message.status_updated",
  "timestamp": "2026-09-24T14:31:12+00:00",
  "company_id": 1234567890,
  "data": {
    "message_id": "msg_6f0a2c",
    "direction": "outgoing",
    "channel": "whatsapp",
    "from": "+17185550100",
    "to": "+17185550142",
    "body": "",
    "status": "read",
    "error": null,
    "sent_at": "2026-09-24 14:31:10",
    "attachments": []
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## fax.sent

An outgoing fax is in progress.

An outgoing fax was handed to the fax carrier and is in progress.

We look for new faxes on a schedule. Each fax is read once, when it first appears, and the event is chosen by its status at that moment. A later change sends no further event: a fax that finishes after we first saw it sends no `fax.delivered` or `fax.failed`. Read the final status with `GET /v1/faxes`.

Known issue: `sent_at` is Eastern time written with a `Z`, so it reads 4 or 5 hours early.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `fax.sent`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.fax_id` | string | yes | Use it with `GET /v1/media/{id}` once the document is ready. Matches `^fax_[0-9]+$`. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.from` | string | yes | E.164. |
| `data.to` | string | yes | E.164. |
| `data.pages` | integer or null | yes |  |
| `data.subject` | string or null | yes |  |
| `data.status` | string | yes | One of `in_progress`, `completed`, `failed`, `restricted` or `unknown`. |
| `data.faxage_job_id` | string or null | yes | The fax carrier's job id. |
| `data.sent_at` | string (date-time) or null | yes | When the fax was logged. Known issue: Eastern time written with a `Z`, so it reads 4 or 5 hours early. |

**Example delivery**

```json
{
  "id": "evt_6703a41e2d8b3.48102967",
  "event_type": "fax.sent",
  "timestamp": "2026-09-24T14:06:02+00:00",
  "company_id": 1234567890,
  "data": {
    "fax_id": "fax_77120",
    "direction": "outgoing",
    "from": "+17185550100",
    "to": "+17185550142",
    "pages": null,
    "subject": "Intake form",
    "status": "in_progress",
    "faxage_job_id": "93125501",
    "sent_at": "2026-09-24T10:05:09.000Z"
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## fax.received

An incoming fax is arriving.

An incoming fax was logged while it was still arriving. An incoming fax that was logged already complete comes as `fax.delivered` instead.

We look for new faxes on a schedule. Each fax is read once, when it first appears, and the event is chosen by its status at that moment. A later change sends no further event: a fax that finishes after we first saw it sends no `fax.delivered` or `fax.failed`. Read the final status with `GET /v1/faxes`.

Known issue: `sent_at` is Eastern time written with a `Z`, so it reads 4 or 5 hours early.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `fax.received`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.fax_id` | string | yes | Use it with `GET /v1/media/{id}` once the document is ready. Matches `^fax_[0-9]+$`. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.from` | string | yes | E.164. |
| `data.to` | string | yes | E.164. |
| `data.pages` | integer or null | yes |  |
| `data.subject` | string or null | yes |  |
| `data.status` | string | yes | One of `in_progress`, `completed`, `failed`, `restricted` or `unknown`. |
| `data.faxage_job_id` | string or null | yes | The fax carrier's job id. |
| `data.sent_at` | string (date-time) or null | yes | When the fax was logged. Known issue: Eastern time written with a `Z`, so it reads 4 or 5 hours early. |

**Example delivery**

```json
{
  "id": "evt_6703a5a07e4c9.26039174",
  "event_type": "fax.received",
  "timestamp": "2026-09-24T15:39:31+00:00",
  "company_id": 1234567890,
  "data": {
    "fax_id": "fax_77131",
    "direction": "incoming",
    "from": "+12125550187",
    "to": "+17185550100",
    "pages": null,
    "subject": null,
    "status": "in_progress",
    "faxage_job_id": "93125870",
    "sent_at": "2026-09-24T11:39:02.000Z"
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## fax.delivered

A fax completed.

A fax was logged as completed when we first saw it. Fetch the document with `GET /v1/media/{fax_id}`.

We look for new faxes on a schedule. Each fax is read once, when it first appears, and the event is chosen by its status at that moment. A later change sends no further event: a fax that finishes after we first saw it sends no `fax.delivered` or `fax.failed`. Read the final status with `GET /v1/faxes`.

Known issue: `sent_at` is Eastern time written with a `Z`, so it reads 4 or 5 hours early.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `fax.delivered`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.fax_id` | string | yes | Use it with `GET /v1/media/{id}` once the document is ready. Matches `^fax_[0-9]+$`. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.from` | string | yes | E.164. |
| `data.to` | string | yes | E.164. |
| `data.pages` | integer or null | yes |  |
| `data.subject` | string or null | yes |  |
| `data.status` | string | yes | One of `in_progress`, `completed`, `failed`, `restricted` or `unknown`. |
| `data.faxage_job_id` | string or null | yes | The fax carrier's job id. |
| `data.sent_at` | string (date-time) or null | yes | When the fax was logged. Known issue: Eastern time written with a `Z`, so it reads 4 or 5 hours early. |

**Example delivery**

```json
{
  "id": "evt_6703a5b3f1d06.85216403",
  "event_type": "fax.delivered",
  "timestamp": "2026-09-24T15:41:02+00:00",
  "company_id": 1234567890,
  "data": {
    "fax_id": "fax_77135",
    "direction": "incoming",
    "from": "+12125550187",
    "to": "+17185550100",
    "pages": 2,
    "subject": null,
    "status": "completed",
    "faxage_job_id": "93125877",
    "sent_at": "2026-09-24T11:40:00.000Z"
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.

## fax.failed

A fax failed.

A fax was logged as failed or restricted when we first saw it, for example an outgoing fax the carrier refused at once.

We look for new faxes on a schedule. Each fax is read once, when it first appears, and the event is chosen by its status at that moment. A later change sends no further event: a fax that finishes after we first saw it sends no `fax.delivered` or `fax.failed`. Read the final status with `GET /v1/faxes`.

Known issue: `sent_at` is Eastern time written with a `Z`, so it reads 4 or 5 hours early.

### Headers

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `X-Vocatech-Signature` | header | string | yes | `t=<unix seconds>,v1=<hex HMAC-SHA256 of t=<t>.<raw body>, keyed with your secret_key>`. Reject the delivery if `t` is more than 300 seconds from your clock or `v1` does not match. For example `t=1790259302,v1=4e1c9a7b2f5d8e3a6c0b9f2e5d8a1c4b7e0f3a6d9c2b5e8f1a4d7c0b3e6f9a2d`. |
| `X-Vocatech-Event` | header | string | yes | The event type, the same as `event_type` in the body. |
| `User-Agent` | header | string | yes | Always `Vocatech-Webhook/1.0`. Always `Vocatech-Webhook/1.0`. |

### Payload

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | A unique id for this delivery's event. Retries repeat it. Use it to skip duplicates. For example `evt_6703a1f25c9e8.21736450`. |
| `event_type` | string | yes | Always `fax.failed`. |
| `timestamp` | string (date-time) | yes | When we built the event. ISO 8601 with `+00:00`. |
| `company_id` | integer | yes | Your company id. |
| `data` | object | yes |  |
| `data.fax_id` | string | yes | Use it with `GET /v1/media/{id}` once the document is ready. Matches `^fax_[0-9]+$`. |
| `data.direction` | string | yes | One of `incoming` or `outgoing`. |
| `data.from` | string | yes | E.164. |
| `data.to` | string | yes | E.164. |
| `data.pages` | integer or null | yes |  |
| `data.subject` | string or null | yes |  |
| `data.status` | string | yes | One of `in_progress`, `completed`, `failed`, `restricted` or `unknown`. |
| `data.faxage_job_id` | string or null | yes | The fax carrier's job id. |
| `data.sent_at` | string (date-time) or null | yes | When the fax was logged. Known issue: Eastern time written with a `Z`, so it reads 4 or 5 hours early. |

**Example delivery**

```json
{
  "id": "evt_6703a60c4a2e8.60371925",
  "event_type": "fax.failed",
  "timestamp": "2026-09-24T16:02:03+00:00",
  "company_id": 1234567890,
  "data": {
    "fax_id": "fax_77140",
    "direction": "outgoing",
    "from": "+17185550100",
    "to": "+17185550142",
    "pages": null,
    "subject": "Intake form",
    "status": "failed",
    "faxage_job_id": null,
    "sent_at": "2026-09-24T12:01:40.000Z"
  }
}
```

### Your answer

#### 2XX

Received. Answer within 5 seconds, then do the work.

#### Any other status

Any other status, no answer within 5 seconds, or a redirect is a failed delivery. We retry it.
