# How to Handle Incoming Webhook Payloads

When Carbon Voice POSTs to your webhook endpoint, you'll receive a JSON payload containing message details. This guide explains the key fields you'll need to process incoming messages and send replies.

## Normal Payload Shape

Every normal event webhook has this top-level structure:

| Field | Type | Description |
| ----- | ----- | ----- |
| `eventName` | string | The event that triggered this delivery (e.g. `"message.posted.to.channel"`) |
| `subscribedUserIds` | string[] | All subscriber user IDs that are members of the affected resource. **Delivered to all subscribers regardless of their OAuth token state.** |
| `subscribedUserSettings` | object[] | Per-user locale settings (see below) |
| `data` | object | The event resource (see below) |

### subscribedUserSettings fields

Each object in `subscribedUserSettings` includes:

| Field | Description |
| ----- | ----- |
| `user_id` | The subscriber's user ID |
| `defaultLanguage` | The user's preferred language, if set |
| `timezone` | The user's UTC offset in minutes, if set |

### data fields

| Field | Description |
| ----- | ----- |
| `resourceId` | The ID of the resource that triggered the event |
| `resourceType` | The type of resource (e.g. `"Message"`, `"Channel"`) |
| `resource` | The full resource object (shape varies by event — see [Webhook Events Reference](../reference/webhook-events.md)) |

### All subscribers receive every event

**Carbon Voice delivers the normal event to every subscriber regardless of their OAuth token state.** There is no token-validity check that gates normal event delivery.

## Key Fields inside data.resource

When processing an incoming `message.posted.to.channel` event, extract these fields from `data.resource` to use in your reply:

| Field | Use |
| ----- | ----- |
| `conversation_id` | The conversation to reply into |
| `id` | The message ID (use as `reply_to_message_id` to thread your reply) |
| `transcript_txt` | The text of what was said |
| `creator_id` | The user who sent the message (useful for filtering) |
| `attachments` | Array of attached files or links (see below) |

### Attachment fields

Each object in `attachments` includes:

| Field | Description |
| ----- | ----- |
| `id` | Attachment ID — use this to request a signed download URL |
| `type` | `"file"` for uploaded files, `"link"` for web URLs |
| `link` | For `"link"` type: a directly accessible URL. For `"file"` type: an internal storage path — **not** directly downloadable |
| `filename` | Original filename (file attachments only) |
| `mime_type` | MIME type (file attachments only) |
| `length_in_bytes` | File size in bytes (file attachments only) |

> **File attachments are not pre-signed.** To download a file, exchange its `id` for a short-lived signed URL — see [How to Download a File Attachment](./how-to-download-a-file-attachment.md).

## Example Payload Structure

```json
{
  "eventName": "message.posted.to.channel",
  "subscribedUserIds": ["user-uuid-1", "user-uuid-2"],
  "subscribedUserSettings": [
    {
      "user_id": "user-uuid-1",
      "defaultLanguage": "en",
      "timezone": -180
    },
    {
      "user_id": "user-uuid-2",
      "defaultLanguage": "pt",
      "timezone": -180
    }
  ],
  "data": {
    "resourceId": "message-uuid",
    "resourceType": "Message",
    "resource": {
      "conversation_id": "channel-uuid",
      "id": "message-uuid",
      "transcript_txt": "Hello, how can I help you?",
      "creator_id": "user-uuid-1",
      "attachments": []
    }
  }
}
```

## Common Patterns

### Reply to a Message

Use `data.resource.conversation_id` to identify the conversation and `data.resource.id` as `reply_to_message_id` to thread your reply:

```bash
curl -X POST https://api.carbonvoice.app/v5/messages/text \
  -H "Authorization: Bearer <your-pat>" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "<data.resource.conversation_id from webhook>",
    "transcript": "Here is my response",
    "reply_to_message_id": "<data.resource.id from webhook>"
  }'
```

### Filter Out Your Own Messages

To prevent infinite reply loops, filter out messages sent by your agent:

```javascript
if (webhook.data.resource.creator_id === myAgentUserId) {
  // Ignore this message, it's from us
  return;
}
```

Or use webhook subscription filters (see [How to Register for Webhooks](./how-to-register-for-webhooks.md)).

## Full Payload Reference

For the complete webhook payload structure and all available event types, see:
- [Webhook Events Reference](../reference/webhook-events.md)
- [cv-contracts repository](https://github.com/PhononX/cv-contracts/tree/main/src/schemas/webhook)
