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.

An inbound hook is a unique address that accepts data and turns it into an event. Nothing happens to that data until a workflow with the hook as its trigger picks it up and decides what to create or update.

POST, no auth

POST + Bearer key

Bcc

HTML form

Form hook

Your server or a partner

Web hook

Order confirmation email

Email hook

kustomer.hooks.<type>.<id>.receive

Workflow

Customers, conversations,
custom objects…

Diagram source
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 hookWeb hookEmail hook
Addresshttps://…/v1/hooks/form/{org}/{hash}https://…/v1/hooks/web/{hash}{hash}@hooks.kustomerapp.com
AuthenticationNoneAuthorization: Bearer <key> with the org.hooks roleNone (anyone who knows the address)
AcceptsJSON, form data (including files), XMLJSON, form data (including files), XMLEmail; JSON and JSON-LD in the HTML are parsed
Best forPublic web formsServer-to-server, partner integrationsOrder and shipping emails you already send
reCAPTCHASupported on the {org}/{hash} endpoint——

Create a hook

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

post/v1/hooks/web
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" }'
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 and POST /v1/hooks/email.

Send data

Shell
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.

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

200 OK
{ "data": { "id": "5825bf0fbe3af71000ac6427", "type": "transaction" } }

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). The trigger's output is the transaction, and what you sent is under attributes.data:

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>:

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.

Debug a hook

Turn on debugging to see exactly what Kustomer received:

Shell
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.

ProblemFix
401 from a web hookSend the key as Authorization: Bearer …, and give it the org.hooks role
413Keep the body under 400 KB; send large files to attachments instead
Transaction created but nothing happensNo enabled workflow uses this hook as its trigger, or its first condition didn't match. Check the workflow logs
Fields are missing in the workflowReference 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. The app's workflows then use those event names as triggers.