# Create Outbound Webhook

> POST /v1/outbound-webhooks · Apps platform API

Source: https://developer.kustomer.com/api/apps-platform/create-outbound-webhook

```http
POST https://{orgname}.api.kustomerapp.com/v1/outbound-webhooks
Authorization: Bearer <API key>
```

## Authorization

Requires any one of these roles: `org.outbound_webhooks`, `org.permission.outbound_webhook.create`; or legacy `org.admin.outbound_webhooks`. The API key or user must hold at least one of them. Legacy roles predate permission sets and still work.

Creates an outbound web hook.

## About Outbound webhooks

An outbound webhook is a subscription that tells Kustomer to send certain events, such as a new conversation or an updated customer, to your HTTPS endpoint as they happen. Transactions are the log of recent delivery attempts, used to debug failures.

An outbound webhook pairs a `url` with a list of `events`. Whenever 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 JSON `POST`. This is the opposite direction from [inbound hooks](https://developer.kustomer.com/api/apps-platform#hooks-web-hooks), which are Kustomer-hosted addresses that receive your data. Admins manage webhooks in **Settings › Platform › Outbound webhooks**, and apps can create them at install time through the `outboundWebhooks` section of an [app definition](https://developer.kustomer.com/docs/apps/app-definition). A webhook keeps delivering until you disable or delete it, or until Kustomer disables it because your endpoint keeps failing.

#### Key fields

| Field | Meaning |
| --- | --- |
| `name` | Display name, up to 255 characters |
| `url` | Your endpoint. It must use `https://`. |
| `events` | Up to 12 event names, such as `kustomer.conversation.update`. `GET /v1/outbound-webhooks/events` lists the ones you can subscribe to. |
| `headers` | Extra `{ name, value }` headers sent with every delivery, such as a shared secret your gateway checks |
| `enabled` | Whether Kustomer sends deliveries. Defaults to `true`. |
| `isError` | `true` when Kustomer has disabled the webhook automatically after sustained failures |
| `token` | The secret Kustomer uses to sign each delivery |

#### Signing and delivery

Each delivery has an `X-Kustomer-Webhook-Signature` header: the hex HMAC-SHA256 of the raw request body, keyed with the webhook's `token`. Verify it before trusting the body. Delivery is at least once and not ordered, so deduplicate on the event `id` and ignore snapshots older than the one you have. Kustomer retries failed deliveries for a while, then drops the event. If failures continue, it sets `enabled` to `false` and `isError` to `true`, and events that happen while a webhook is off are not queued for later. See [Delivery and security](https://developer.kustomer.com/docs/events/delivery-and-retries) for the retry schedule and how to reconcile after an outage.

#### Transactions

A transaction is the record of one delivery attempt. It holds the `webhookId`, the `eventName` sent, `sentAt` (a millisecond timestamp), `status` (`succeeded` or `failed`) and `nextRetry`, the millisecond timestamp of the next attempt, which is `null` once the event succeeded or ran out of attempts. Transactions don't include the payload. Only the 25 most recent attempts are returned, for one webhook or across all of them, so treat them as a debugging view rather than an audit trail.

#### Which endpoint to use

| To | Call |
| --- | --- |
| Find the events you can subscribe to | [`GET /v1/outbound-webhooks/events`](https://developer.kustomer.com/api/apps-platform/list-subscribable-events) |
| Create a webhook | [`POST /v1/outbound-webhooks`](https://developer.kustomer.com/api/apps-platform/create-outbound-webhook) |
| List webhooks | [`GET /v1/outbound-webhooks`](https://developer.kustomer.com/api/apps-platform/list-outbound-webhooks) |
| Read one webhook, including its `token` | [`GET /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/get-outbound-webhook) |
| Change the URL, events or headers, or pause and re-enable it | [`PATCH /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/update-outbound-webhook) |
| Delete a webhook | [`DELETE /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/delete-outbound-webhook) |
| See 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) |

How many webhooks an org can have depends on its plan; creating one beyond the limit fails with `409 Conflict`. There is no endpoint to rotate a `token`: create a second webhook with the same URL and events, accept both tokens while you switch, then delete the old one. After re-enabling a webhook with `PATCH { "enabled": true }`, reconcile anything you missed with [incremental search](https://developer.kustomer.com/docs/guides/search-recipes). Each endpoint accepts the matching outbound webhook permission, such as `org.permission.outbound_webhook.create` for creating, or the outbound webhooks admin role.

Related: [Outbound webhooks guide](https://developer.kustomer.com/docs/events/outbound-webhooks), [Delivery and security](https://developer.kustomer.com/docs/events/delivery-and-retries), [Event catalog](https://developer.kustomer.com/docs/events/event-catalog), [Inbound hooks](https://developer.kustomer.com/api/apps-platform#hooks-web-hooks)

### Recipes

#### 1. Subscribe your endpoint to conversation changes

Have Kustomer push new and updated conversations to your service as they happen, instead of polling for changes.

**Step 1.** [`GET /v1/outbound-webhooks/events`](https://developer.kustomer.com/api/apps-platform/list-subscribable-events.md). Check the event names you can subscribe to. Only events in this list are accepted, and a webhook can hold up to 12 of them.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/outbound-webhooks/events" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/outbound-webhooks/events
```

**Step 2.** [`POST /v1/outbound-webhooks`](https://developer.kustomer.com/api/apps-platform/create-outbound-webhook.md). Create the webhook with `name`, an `https://` `url` and the `events`. Store the `token` from the response in your secret store: it's the key for checking the `X-Kustomer-Webhook-Signature` header on every delivery. If the org already has as many webhooks as its plan allows, you get `409 Conflict`.

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

```bash title="kusty"
kusty api POST /v1/outbound-webhooks \
  --field name="Conversation sync" --field url=https://hooks.example.com/kustomer \
  --field 'events:=["kustomer.conversation.create","kustomer.conversation.update"]' \
  --fields id,attributes.token
```

**Step 3.** [`GET /v1/outbound-webhooks/{webhookId}/transactions`](https://developer.kustomer.com/api/apps-platform/list-transactions-for-webhook.md). Change a conversation in Kustomer, then check the delivery attempts. `status` is `succeeded` or `failed`, and `sentAt` is a millisecond timestamp. Transactions don't include the payload, so check your own logs for what arrived.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/outbound-webhooks/$WEBHOOK_ID/transactions" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/outbound-webhooks/$WEBHOOK_ID/transactions --all \
  | jq -r '[.data.attributes.eventName, .data.attributes.status, .data.attributes.sentAt] | @tsv'
```

> [Delivery and security](https://developer.kustomer.com/docs/events/delivery-and-retries) shows how to verify the signature and handle retries, duplicates and ordering.

#### 2. Recover a webhook that Kustomer turned off

Find webhooks that were disabled automatically after your endpoint kept failing, switch them back on, and catch up on what you missed.

**Step 1.** [`GET /v1/outbound-webhooks`](https://developer.kustomer.com/api/apps-platform/list-outbound-webhooks.md). List the webhooks and look for `isError: true`. Those were turned off by Kustomer, not by a person, and `enabled` is `false` on them.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/outbound-webhooks" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/outbound-webhooks --all \
  | jq -r 'select(.data.attributes.isError) | [.data.id, .data.attributes.name, .data.attributes.url] | @tsv'
```

**Step 2.** [`GET /v1/outbound-webhooks/{webhookId}/transactions`](https://developer.kustomer.com/api/apps-platform/list-transactions-for-webhook.md). Look at the most recent attempts to see when deliveries started failing, which tells you where your catch-up window should begin. Only the 25 most recent attempts are returned.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/outbound-webhooks/$WEBHOOK_ID/transactions" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/outbound-webhooks/$WEBHOOK_ID/transactions --all \
  | jq -r 'select(.data.attributes.status == "failed") | .data.attributes.sentAt' | sort -n | head -1
```

**Step 3.** [`PATCH /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/update-outbound-webhook.md). After fixing the endpoint, send `{ "enabled": true }`. That also sets `isError` back to `false` and clears the failure history.

```bash title="cURL"
curl --request PATCH "https://api.kustomerapp.com/v1/outbound-webhooks/$WEBHOOK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"enabled": true}'
```

```bash title="kusty"
kusty api PATCH /v1/outbound-webhooks/$WEBHOOK_ID --field enabled:=true --fields attributes.enabled,attributes.isError
```

**Step 4.** Events that happened while the webhook was off are never sent. Fetch the records that changed since the first failure with [search](https://developer.kustomer.com/docs/guides/search-recipes), and let your handler skip snapshots older than the ones you already have.

#### 3. Add an event to a webhook without dropping the others

Start receiving another kind of event on an existing webhook, keeping its current subscriptions.

**Step 1.** [`GET /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/get-outbound-webhook.md). Read the webhook's current `events`.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/outbound-webhooks/$WEBHOOK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
EVENTS=$(kusty api GET /v1/outbound-webhooks/$WEBHOOK_ID \
  | jq -c '.data.attributes.events + ["kustomer.message.create"] | unique')
```

**Step 2.** [`PATCH /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/update-outbound-webhook.md). Send the full list back. `PATCH` changes only the fields you send, but `events` (like `headers`) replaces the stored list, so anything you leave out stops being delivered. The list can hold up to 12 unique events. Webhooks that an app created only accept changes to `enabled`.

```bash title="cURL"
curl --request PATCH "https://api.kustomerapp.com/v1/outbound-webhooks/$WEBHOOK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"events\": $EVENTS}"
```

```bash title="kusty"
kusty api PATCH /v1/outbound-webhooks/$WEBHOOK_ID --field events:="$EVENTS" --fields attributes.events
```

#### 4. Rotate a webhook's signing token

Replace the secret your receiver uses to verify deliveries, for example after it may have leaked, without missing events while you switch.

**Step 1.** [`GET /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/get-outbound-webhook.md). Read the old webhook's `url`, `events` and `headers`. There's no way to change a webhook's `token`, so the new token comes from a new webhook.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/outbound-webhooks/$OLD_WEBHOOK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/outbound-webhooks/$OLD_WEBHOOK_ID \
  | jq '.data.attributes | {name: (.name + " (new token)"), url, events, headers}' > webhook.json
```

**Step 2.** [`POST /v1/outbound-webhooks`](https://developer.kustomer.com/api/apps-platform/create-outbound-webhook.md). Create a copy and store its `token`. Update your receiver to accept a signature made with either token. While both webhooks exist, every event is delivered twice, once by each, so deduplicate on the event `id`.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/outbound-webhooks" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data @webhook.json
```

```bash title="kusty"
kusty api POST /v1/outbound-webhooks --data @webhook.json --fields id,attributes.token
```

**Step 3.** [`DELETE /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/delete-outbound-webhook.md). Once deliveries from the new webhook verify, delete the old one and drop the old token from your receiver.

```bash title="cURL"
curl --request DELETE "https://api.kustomerapp.com/v1/outbound-webhooks/$OLD_WEBHOOK_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api DELETE /v1/outbound-webhooks/$OLD_WEBHOOK_ID --confirm --yes
```

> If the org is at its plan's webhook limit, creating the copy fails with `409 Conflict`. Disabled webhooks still count toward the limit and webhooks that apps create don't, so delete one you no longer use first.

### Example object

A real record from [`GET /v1/outbound-webhooks/{id}`](https://developer.kustomer.com/api/apps-platform/get-outbound-webhook.md):

```json
{
  "id": "5f3e8f9b2b3fc60013f6b836",
  "type": "outbound-webhook",
  "attributes": {
    "name": "Conversation Sync",
    "enabled": true,
    "url": "https://example.com",
    "isError": false,
    "events": [
      "kustomer.conversation.create",
      "kustomer.conversation.update",
      "kustomer.message.create"
    ],
    "headers": [
      {
        "name": "foo",
        "value": "bar"
      }
    ],
    "token": "f1253ac0dacd0a8fdbc5b8caac814865b53b164acb83f9163d5adad0195a67714be43d778a9118d98e24953d878d20a7ab2ccf8667c02eeda82cb8fcc0b42ba",
    "createdAt": "2020-08-20T14:58:35.499Z",
    "updatedAt": "2020-08-20T14:58:35.499Z"
  },
  "relationships": {
    "org": {
      "data": {
        "id": "5e45b1921e554100197c6b31",
        "type": "org"
      },
      "links": {
        "self": "/v1/orgs/5e45b1921e554100197c6b31"
      }
    },
    "createdBy": {
      "data": {
        "id": "5f0f56d3fee2930019723507",
        "type": "user"
      },
      "links": {
        "self": "/v1/users/5f0f56d3fee2930019723507"
      }
    }
  },
  "links": {
    "self": "/v1/outbound-webhooks/5f3e8f9b2b3fc60013f6b836"
  }
}
```

Fields:

- `attributes.name` (string): Display name, up to 255 characters.
- `attributes.enabled` (boolean): Whether Kustomer sends deliveries. Defaults to `true`. Events that happen while it's `false` are never sent, even after you re-enable it.
- `attributes.url` (string): Your endpoint. It must use `https://` and be at most 255 characters. Redirects aren't followed, so use the final URL.
- `attributes.isError` (boolean): `true` when Kustomer turned the webhook off after sustained delivery failures; `enabled` is then `false` too. Sending `{ "enabled": true }` clears it.
- `attributes.events` (array): Up to 12 unique event names from `GET /v1/outbound-webhooks/events`. `PATCH` replaces the whole list, so send every event you want to keep.
- `attributes.headers` (array): Extra headers sent with every delivery, as `{ name, value }`. `PATCH` replaces the whole list.
- `attributes.headers[].name` (string): Up to 100 characters.
- `attributes.headers[].value` (string): Up to 255 characters. It's returned in plain text to anyone who can read webhooks, so treat the signature, not a header, as proof that a delivery came from Kustomer.
- `attributes.token` (string): The HMAC-SHA256 key for the `X-Kustomer-Webhook-Signature` header on each delivery. Kustomer generates it and it can't be changed; to rotate it, create a new webhook.

### Permissions

Fewest roles for a read-only integration: `org.outbound_webhooks`. For every endpoint: `org.outbound_webhooks`.

## Request body

Content type: `application/json`

Example:

```json
{
  "name": "Conversation Sync",
  "url": "https://example.com",
  "events": [
    "kustomer.conversation.create",
    "kustomer.conversation.update",
    "kustomer.message.create"
  ],
  "headers": [
    {
      "name": "foo",
      "value": "bar"
    }
  ]
}
```

## Responses

### 201

Success

- `data` (object, required)
  - `id` (string, required)
  - `type` (string, required)
  - `attributes` (object, required)
    - `name` (string, required)
    - `enabled` (boolean, required)
    - `url` (string, required)
    - `isError` (boolean, required)
    - `events` (array<string>, required)
    - `headers` (array<object>, required)
    - `token` (string, required)
    - `createdAt` (string, required)
    - `updatedAt` (string, required)
  - `relationships` (object, required)
    - `org` (object, required)
    - `createdBy` (object, required)
  - `links` (object, required)
    - `self` (string, required)

```json
{
  "data": {
    "id": "5f3e8f9b2b3fc60013f6b836",
    "type": "outbound-webhook",
    "attributes": {
      "name": "Conversation Sync",
      "enabled": true,
      "url": "https://example.com",
      "isError": false,
      "events": [
        "kustomer.conversation.create",
        "kustomer.conversation.update",
        "kustomer.message.create"
      ],
      "headers": [
        {
          "name": "foo",
          "value": "bar"
        }
      ],
      "token": "f20341ae0ddcd0a8fdbc5b8caac814865b53b164acb83f9163d5adad0195a67714be43d778a9118d98e24953d878d20a7ab2ccf8667c02eeda82cb8fcc0b44aa",
      "createdAt": "2020-08-20T14:58:35.499Z",
      "updatedAt": "2020-08-20T14:58:35.499Z"
    },
    "relationships": {
      "org": {
        "data": {
          "id": "5e45b1921e554100197c6b31",
          "type": "org"
        },
        "links": {
          "self": "/v1/orgs/5e45b1921e554100197c6b31"
        }
      },
      "createdBy": {
        "data": {
          "id": "5f0f56d3fee2930019723507",
          "type": "user"
        },
        "links": {
          "self": "/v1/users/5f0f56d3fee2930019723507"
        }
      }
    },
    "links": {
      "self": "/v1/outbound-webhooks/5f3e8f9b2b3fc60013f6b836"
    }
  }
}
```

### 400

Bad Request

### 401

Unauthorized

### 403

Forbidden

### 409

Conflict

## Example request

```bash
curl --request POST \
  --url 'https://api.kustomerapp.com/v1/outbound-webhooks' \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "Conversation Sync",
  "url": "https://example.com",
  "events": [
    "kustomer.conversation.create",
    "kustomer.conversation.update",
    "kustomer.message.create"
  ],
  "headers": [
    {
      "name": "foo",
      "value": "bar"
    }
  ]
}'
```
