Documentation
Webhooks
Verified messaging and funding webhook contracts, duplicate handling, and current reconciliation behavior.
Webhooks
Textree 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 Textree uses to update message, opt-out, and funding state.
Customer event types
Subscribe your endpoint to the events your application needs:
message.sentmessage.deliveredmessage.failedmessage.receivedconversation.createdconversation.updatedopt_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.
{
"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
- Peer requests must include
x-peer-signature - Signature values use the format
sha256=<hex>
The signature is computed as an HMAC-SHA256 over Textree’s canonicalized payload representation, not over raw request bytes.
Accepted response
A verified webhook that passes shape validation returns 202 Accepted:
{
"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, Textree returns the
existing record with "duplicate": true and does not enqueue duplicate downstream work.
Failure responses
401with{"error":"invalid_signature"}when verification fails422with{"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.
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.sentmessage.deliveredmessage.failed
These update downstream message state and may populate provider identifiers on the stored message.
Reconciled opt-out events
recipient.opted_outcontact.opted_outmessage.opted_outmessage.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.completedfunding.failedfunding.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
{
"type": "message.delivered",
"id": "provider_evt_123",
"message_id": "msg_123"
}
Messaging opt-out event
{
"type": "recipient.opted_out",
"id": "provider_evt_456",
"phone_number": "+15551234567",
"message_id": "msg_123"
}
Peer funding event
{
"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
/appwith 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.