# Outbound webhooks

> Subscribe an HTTPS endpoint to Kustomer events. Create webhooks with the API, in Settings or from an app definition, then receive signed JSON for every customer, conversation, message, user, team and tag change.

Source: https://developer.kustomer.com/docs/events/outbound-webhooks

An outbound webhook is a URL plus a list of events. When one of those events happens in your org, Kustomer sends the [event envelope](https://developer.kustomer.com/docs/events#the-event-envelope) to the URL as a signed `POST`.

#### Expose an HTTPS endpoint

It must accept `POST` with a JSON body and answer with a `2xx` within a few seconds. Verify the signature before trusting the body.

#### Create the webhook

Use the API, **Settings › Platform › Outbound webhooks**, or the `outboundWebhooks` section of an [app definition](https://developer.kustomer.com/docs/apps/app-definition).

#### Store the token

The response includes a `token`. It's the HMAC key for `X-Kustomer-Webhook-Signature`. Keep it in your secret store.

#### Watch deliveries

List recent delivery attempts per webhook to debug failures.

## Create a webhook with the API

`POST /v1/outbound-webhooks` ([reference](https://developer.kustomer.com/api/apps-platform/create-outbound-webhook))

```bash title="cURL"
curl -X POST "https://acme.api.kustomerapp.com/v1/outbound-webhooks" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Conversation sync",
    "url": "https://hooks.example.com/kustomer",
    "events": ["kustomer.conversation.create", "kustomer.conversation.update", "kustomer.message.create"],
    "headers": [{ "name": "X-Source", "value": "kustomer" }]
  }'
```

```ts title="Node.js"
const res = await fetch('https://acme.api.kustomerapp.com/v1/outbound-webhooks', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.KUSTOMER_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Conversation sync',
    url: 'https://hooks.example.com/kustomer',
    events: ['kustomer.conversation.create', 'kustomer.conversation.update', 'kustomer.message.create'],
    headers: [{ name: 'X-Source', value: 'kustomer' }],
  }),
})
const { data } = await res.json()
await secrets.put('kustomer-webhook-token', data.attributes.token)
```

```python title="Python"
import os, requests

res = requests.post(
    "https://acme.api.kustomerapp.com/v1/outbound-webhooks",
    headers={"Authorization": f"Bearer {os.environ['KUSTOMER_API_KEY']}"},
    json={
        "name": "Conversation sync",
        "url": "https://hooks.example.com/kustomer",
        "events": ["kustomer.conversation.create", "kustomer.conversation.update", "kustomer.message.create"],
        "headers": [{"name": "X-Source", "value": "kustomer"}],
    },
)
token = res.json()["data"]["attributes"]["token"]
```

```bash title="kusty"
cat > webhook.json <<'EOF'
{
  "name": "Conversation sync",
  "url": "https://hooks.example.com/kustomer",
  "events": ["kustomer.conversation.create", "kustomer.conversation.update", "kustomer.message.create"]
}
EOF
kusty api POST /v1/outbound-webhooks --data @webhook.json --output json
```

**Request body**

_An outbound webhook builder. Fill in a name and URL (non-HTTPS URLs show Not a valid absolute HTTPS URL), toggle create and update events for conversations, customers, messages, users, teams and tags, and add custom headers, with a warning when one reuses a header Kustomer sets. The create request updates in cURL, Node.js, Python and app definition form. Beside it, a sample delivery for a chosen event shows the POST with Content-Type, X-Kustomer-Webhook-Event and an HMAC-SHA256 X-Kustomer-Webhook-Signature computed in the browser, then the body. A verify panel takes a webhook token and compares the signature against what a receiver computes from the raw body, a re-serialized body or a pretty-printed body: only the raw body matches._

```json title="201 Created"
{
  "data": {
    "type": "outbound-webhook",
    "id": "5f3e8f9b2b3fc60013f6b836",
    "attributes": {
      "name": "Conversation sync",
      "enabled": true,
      "url": "https://hooks.example.com/kustomer",
      "isError": false,
      "events": ["kustomer.conversation.create", "kustomer.conversation.update", "kustomer.message.create"],
      "headers": [{ "name": "X-Source", "value": "kustomer" }],
      "token": "f1253ac0dacd0a8fdbc5b8caac814865b53b164acb83f9163d5adad0195a6771…",
      "createdAt": "2026-10-21T14:58:35.499Z",
      "updatedAt": "2026-10-21T14:58:35.499Z"
    }
  }
}
```

`isError` is `true` while Kustomer has [automatically disabled](https://developer.kustomer.com/docs/events/delivery-and-retries#automatic-disabling) the webhook.

> **Permissions:** Managing outbound webhooks needs a key whose role includes `org.permission.outbound_webhook`, which administrators have. How many webhooks an org can have depends on its plan.

## Receive events

A minimal receiver verifies the signature against the **raw** body, acknowledges immediately, then processes the event asynchronously.

```ts title="Node.js (Express)"
import crypto from 'node:crypto'
import express from 'express'

const app = express()
const TOKEN = process.env.KUSTOMER_WEBHOOK_TOKEN!

app.post('/kustomer', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', TOKEN).update(req.body).digest('hex')
  const received = req.get('X-Kustomer-Webhook-Signature') ?? ''
  if (received.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
    return res.sendStatus(401)
  }
  res.sendStatus(200)
  queue.publish(JSON.parse(req.body.toString('utf8')))
})
```

```python title="Python (Flask)"
import hashlib, hmac, json, os
from flask import Flask, request, abort

app = Flask(__name__)
TOKEN = os.environ["KUSTOMER_WEBHOOK_TOKEN"].encode()

@app.post("/kustomer")
def kustomer():
    raw = request.get_data()
    expected = hmac.new(TOKEN, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Kustomer-Webhook-Signature", "")):
        abort(401)
    queue.publish(json.loads(raw))
    return "", 200
```

```go title="Go"
func kustomer(w http.ResponseWriter, r *http.Request) {
	raw, _ := io.ReadAll(r.Body)
	mac := hmac.New(sha256.New, []byte(os.Getenv("KUSTOMER_WEBHOOK_TOKEN")))
	mac.Write(raw)
	expected := hex.EncodeToString(mac.Sum(nil))
	if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Kustomer-Webhook-Signature"))) {
		http.Error(w, "bad signature", http.StatusUnauthorized)
		return
	}
	w.WriteHeader(http.StatusOK)
	go handle(raw)
}
```

Read [Delivery and security](https://developer.kustomer.com/docs/events/delivery-and-retries) before going to production. It covers retries, duplicates, ordering and automatic disabling.

## Manage webhooks

| Task                                                     | Endpoint                                                                                                                               |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| List webhooks                                            | [`GET /v1/outbound-webhooks`](https://developer.kustomer.com/api/apps-platform/list-outbound-webhooks)                                 |
| Get one, including its `token`                           | [`GET /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/get-outbound-webhook)                              |
| Change URL, events or headers, or set `enabled`          | [`PATCH /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/update-outbound-webhook)                         |
| Delete                                                   | [`DELETE /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/delete-outbound-webhook)                        |
| List events you can subscribe to                         | [`GET /v1/outbound-webhooks/events`](https://developer.kustomer.com/api/apps-platform/list-subscribable-events)                        |
| See the 25 most recent delivery attempts for one webhook | [`GET /v1/outbound-webhooks/{webhookId}/transactions`](https://developer.kustomer.com/api/apps-platform/list-transactions-for-webhook) |
| See recent delivery attempts across all webhooks         | [`GET /v1/outbound-webhooks/transactions`](https://developer.kustomer.com/api/apps-platform/list-transactions)                         |

Pause a webhook during maintenance with `PATCH { "enabled": false }`. Events that happen while it's disabled are not queued for later, so reconcile afterwards with [incremental sync](https://developer.kustomer.com/docs/guides/search-recipes#incremental-sync).

## Define webhooks in an app

Apps can create webhooks when they're installed. Add an `outboundWebhooks` array to the app definition with the same fields:

```json title="app definition"
{
  "app": "ecommstore",
  "version": "1.2.0",
  "title": "E-comm Store",
  "outboundWebhooks": [
    {
      "name": "Send conversation events to E-comm Store",
      "url": "https://api.ecommstore.example/kustomer/events",
      "headers": [{ "name": "X-Source", "value": "kustomer" }],
      "events": ["kustomer.conversation.create", "kustomer.conversation.update"]
    }
  ]
}
```

> **Public apps are reviewed:** If a public app uses outbound webhooks, they must be essential to the app, and Kustomer may ask for a data-handling agreement and security details before listing it. See [Marketplace submission](https://developer.kustomer.com/docs/apps/marketplace).

## Webhooks or workflows?

Outbound webhooks send every event of the subscribed types, with a fixed payload. If you only care about a subset ("conversations tagged `vip` that just closed") or need a different payload shape, use a [workflow](https://developer.kustomer.com/docs/workflows) with a condition and a [REST API step](https://developer.kustomer.com/docs/workflows/code-steps) instead. Workflows can also react to events webhooks don't offer, such as custom objects and notes.
