# Webhooks

Canonical URL: https://texttree.ai/en/docs/webhooks/
Markdown URL: https://texttree.ai/en/docs/webhooks.md
Page type: docs
Translation status: approved
Legal status: not_required

## Summary

Verified messaging and funding webhook contracts, duplicate handling, and current reconciliation behavior.

## Source content

# Webhooks

TextTree currently accepts provider callbacks from two boundaries:

- messaging provider events
- Peer for funding events

All webhook routes are JSON endpoints served by Phoenix.

## Endpoints

- signed messaging provider event callbacks
- signed messaging provider inbound callbacks
- `POST /webhooks/peer/events`

Customer webhook endpoints are configured from the authenticated Webhooks page. Provider webhook
routes above are the inbound provider callbacks TextTree uses to update message, opt-out, and
funding state.

## Customer event types

Subscribe your endpoint to the events your application needs:

- `message.sent`
- `message.delivered`
- `message.failed`
- `message.received`
- `conversation.created`
- `conversation.updated`
- `opt_out.created`

## Customer event shape

Customer webhook payloads use a stable envelope so receivers can route by `type` and inspect the
domain object in `data`.

```json
{
  "id": "evt_123",
  "type": "message.delivered",
  "created_at": "2026-04-28T14:00:00Z",
  "data": {
    "message": {
      "id": "msg_123",
      "status": "delivered",
      "phone_number": "+15551234567",
      "segments": 1,
      "cost_cents": 1
    }
  }
}
```

Return any `2xx` response to mark delivery as successful. Non-`2xx` responses are retained for
debugging and retry/replay from the console.

## Signature verification

Webhook verification is mandatory in the current alpha.

- Messaging provider requests must include the configured signature header
- SMS provider requests must include the configured timestamp and signature headers
- Peer requests must include `x-peer-timestamp` and `x-peer-signature`
- Signature values use the format `sha256=<hex>`

The signature is computed as an HMAC-SHA256 over the timestamp and TextTree's canonicalized payload
representation, not over raw request bytes. TextTree rejects missing, stale, future, or mismatched
timestamp/signature pairs; the default freshness window is five minutes.

## Accepted response

A verified webhook that passes shape validation returns `202 Accepted`:

```json
{
  "id": "8fca25fd-d7b7-4f58-8bdd-d4ab4ca3ae97",
  "status": "received",
  "provider": "messaging_provider",
  "event_type": "message.delivered",
  "external_id": "provider_evt_123",
  "duplicate": false
}
```

If the same provider sends the same `event_type` and `external_id` again, TextTree returns the
existing record with `"duplicate": true` and does not enqueue duplicate downstream work.

## Failure responses

- `401` with `{"error":"invalid_signature"}` when verification fails
- `422` with `{"error":"invalid_webhook_payload"}` when the request is signed but missing the
  required event identity fields

## Replay from the console

Every failed customer webhook delivery should be replayable after you fix the endpoint. The replay
flow keeps the original payload and records a new delivery attempt with response code, latency, and
response body.

```txt
Webhooks → select endpoint → open failed delivery → Replay webhook
```

## Messaging event handling

Messaging provider payloads are normalized from keys such as `type`, `id`, `message_id`, and `metadata`.

### Reconciled message events

- `message.sent`
- `message.delivered`
- `message.failed`

These update downstream message state and may populate provider identifiers on the stored message.

### Reconciled opt-out events

- `recipient.opted_out`
- `contact.opted_out`
- `message.opted_out`
- `message.unsubscribed`

In V1, these create or update workspace-level suppressions with provider metadata. Workspace
suppressions block one-off UI sends, developer API sends, MCP sends, and campaign deliveries for
that phone number.

## Peer event handling

Peer payloads are normalized from keys such as `event`, `event_id`, `session_id`, `amount_cents`,
and `metadata`.

### Reconciled funding events

- `funding.completed`
- `funding.failed`
- `funding.expired`

Completed funding events update funding-session state and create idempotent ledger credits.
Failed and expired funding events update the stored funding-session state for operator review.

## Examples

### Messaging delivered event

```json
{
  "type": "message.delivered",
  "id": "provider_evt_123",
  "message_id": "msg_123"
}
```

### Messaging opt-out event

```json
{
  "type": "recipient.opted_out",
  "id": "provider_evt_456",
  "phone_number": "+15551234567",
  "message_id": "msg_123"
}
```

### Peer funding event

```json
{
  "event": "funding.completed",
  "event_id": "peer_evt_123",
  "session_id": "session_123",
  "amount_cents": 500
}
```

## Operational notes

- Provider events are persisted before asynchronous reconciliation.
- Failed events remain visible in `/app` with failure reasons and replay controls.
- Replay is intended for operator recovery after underlying data or provider issues are fixed.
- Unknown but validly signed events may be stored and ignored until the domain layer supports them.
