# Inbound webhooks

> Send data into Kustomer from forms, servers and email with form hooks, web hooks and email hooks. Each hook gets a unique address and triggers workflows with the data you send.

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

An inbound hook is a unique address that accepts data and turns it into an event. Nothing happens to that data until a [workflow](https://developer.kustomer.com/docs/workflows) with the hook as its trigger picks it up and decides what to create or update.

```mermaid
flowchart LR
  F[HTML form] -- "POST, no auth" --> FH[Form hook]
  S[Your server or a partner] -- "POST + Bearer key" --> WH[Web hook]
  E[Order confirmation email] -- "Bcc" --> EH[Email hook]
  FH & WH & EH --> EV{{"kustomer.hooks.&lt;type&gt;.&lt;id&gt;.receive"}}
  EV --> WF[Workflow]
  WF --> R[(Customers, conversations,<br/>custom objects…)]
```

|                | Form hook                                | Web hook                                                | Email hook                                     |
| -------------- | ---------------------------------------- | ------------------------------------------------------- | ---------------------------------------------- |
| Address        | `https://…/v1/hooks/form/{org}/{hash}`   | `https://…/v1/hooks/web/{hash}`                         | `{hash}@hooks.kustomerapp.com`                 |
| Authentication | None                                     | `Authorization: Bearer <key>` with the `org.hooks` role | None (anyone who knows the address)            |
| Accepts        | JSON, form data (including files), XML   | JSON, form data (including files), XML                  | Email; JSON and JSON-LD in the HTML are parsed |
| Best for       | Public web forms                         | Server-to-server, partner integrations                  | Order and shipping emails you already send     |
| reCAPTCHA      | Supported on the `{org}/{hash}` endpoint | —                                                       | —                                              |

> **Use the right hook for untrusted senders:** Form hooks and email hooks accept data from anyone who has the address. Treat everything they deliver as untrusted input: validate it in the workflow before creating or updating records. Use a web hook when you need to know the sender.

## Create a hook

Create hooks in **Settings › Platform › Inbound webhooks**, or with the API:

`POST /v1/hooks/web` ([reference](https://developer.kustomer.com/api/apps-platform/create-web-hook))

```bash title="cURL"
curl -X POST "https://acme.api.kustomerapp.com/v1/hooks/web" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Shop orders", "description": "Order events from the storefront" }'
```

```json title="200 OK"
{
  "data": {
    "type": "web_hook",
    "id": "5825c452be3af71000ac6462",
    "attributes": {
      "description": "Order events from the storefront",
      "eventName": "kustomer.hooks.web.5825c452be3af71000ac6462.receive",
      "hash": "7733e0aab921040f15aa203d89ef8809a015c33d5303bd3642972d59602e43a6",
      "url": "https://acme.api.kustomerapp.com/v1/hooks/web/7733e0aab921040f15aa203d89ef8809a015c33d5303bd3642972d59602e43a6",
      "debug": false
    }
  }
}
```

Use the returned `url` (or `email`, for email hooks) exactly; don't build it yourself. `eventName` is the workflow trigger. The equivalent endpoints for the other types are [`POST /v1/hooks/form`](https://developer.kustomer.com/api/apps-platform/create-form-hook) and [`POST /v1/hooks/email`](https://developer.kustomer.com/api/apps-platform/create-email-hook).

## Send data

**Web hook**

```bash
curl -X POST "$HOOK_URL" \
  -H "Authorization: Bearer $KUSTOMER_HOOK_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "4417", "email": "jane@example.com", "status": "shipped", "tracking": "1Z999AA10123456784" }'
```

Create a dedicated API key with only the `org.hooks` role for each sender. It can post to hooks and nothing else, so a leaked key can't read your data. Send it in the `Authorization` header; a key in the query string returns `401`.

**Form hook**

```html
<form action="https://acme.api.kustomerapp.com/v1/hooks/form/5f8481c6a22ab5b9d5f2dd8a/b6ffcac0…8e3a" method="POST" enctype="multipart/form-data">
  <input name="name" required />
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>
  <input name="screenshot" type="file" accept="image/*" />
  <button>Send</button>
</form>
```

Prefer the `/v1/hooks/form/{org}/{hash}` form of the address over `/v1/hooks/form/{hash}`; it's the only one that supports reCAPTCHA.

**Email hook**

Add the hook address as a Bcc on emails you already send, such as order confirmations. Put structured data in the HTML as JSON-LD:

```html
<script type="application/ld+json">
{
  "@context": "http://schema.org",
  "@type": "Order",
  "orderNumber": "4417",
  "orderStatus": "http://schema.org/OrderInTransit",
  "customer": { "@type": "Person", "email": "jane@example.com" }
}
</script>
```

The workflow receives the parsed email (sender, recipients, subject, text and HTML bodies, headers and attachments) along with any JSON found in the markup.

Every hook answers with the ID of the transaction it created:

```json title="200 OK"
{ "data": { "id": "5825bf0fbe3af71000ac6427", "type": "transaction" } }
```

> **Limits:** Request bodies can be up to **400 KB**. Larger requests fail with `413`. Uploaded files must have a content type, or they're skipped.

## Use the data in a workflow

Pick the hook as the workflow's trigger (**Trigger app › Hooks** in the builder, or its `eventName` in [workflow JSON](https://developer.kustomer.com/docs/workflows/workflow-json)). The trigger's output is the transaction, and what you sent is under `attributes.data`:

```json title="Step 1 output"
{
  "id": "5825cb4af5fefd1100f9c7c9",
  "type": "transaction",
  "attributes": {
    "createdAt": "2026-10-21T15:20:42.554Z",
    "data": {
      "orderId": "4417",
      "email": "jane@example.com",
      "status": "shipped",
      "tracking": "1Z999AA10123456784"
    }
  }
}
```

So the fields you posted are referenced as `/#steps.1.attributes.data.<field>`:

```json title="Workflow step"
{
  "id": "find-customer",
  "action": "kustomer.customer.find-by-email",
  "params": { "email": "/#steps.1.attributes.data.email" },
  "transitions": [{ "target": "upsert-order", "condition": { "op": "exists", "values": ["/#steps.find-customer.id"] } }]
}
```

Files posted as form data become arrays of attachment descriptors (`_id`, `name`, `contentType`, `contentLength`) under the field name, ready to attach to a message or note.

> **Build against a real payload:** Send one realistic request, open the workflow's logs, and copy step 1's output into the builder's **Example webhook data**. The builder then offers every field in its dropdowns.

## Debug a hook

Turn on debugging to see exactly what Kustomer received:

```bash
curl -X PATCH "https://acme.api.kustomerapp.com/v1/hooks/web/$HOOK_ID" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY" -H "Content-Type: application/json" \
  -d '{ "debug": true }'

curl "https://acme.api.kustomerapp.com/v1/hooks/web/$HOOK_ID/transactions" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY"
```

Transactions received while debugging is on stay visible for **24 hours**. The same view is in **Settings › Platform › Inbound webhooks › View debug logs**.

| Problem                                 | Fix                                                                                                                           |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `401` from a web hook                   | Send the key as `Authorization: Bearer …`, and give it the `org.hooks` role                                                   |
| `413`                                   | Keep the body under 400 KB; send large files to [attachments](https://developer.kustomer.com/docs/guides/attachments) instead |
| Transaction created but nothing happens | No enabled workflow uses this hook as its trigger, or its first condition didn't match. Check the workflow logs               |
| Fields are missing in the workflow      | Reference them under `attributes.data`, and check the exact names in a debug transaction                                      |

## Hooks in app definitions

Apps can create hooks at install time, with their own event names, through the `hooks` section of an [app definition](https://developer.kustomer.com/docs/apps/app-definition). The app's workflows then use those event names as triggers.
