Webhooks
Webhooks
Webhooks allow external systems to trigger actions in Hutly by making HTTP POST requests to webhook endpoints.
Overview
Webhooks in Hutly are incoming webhooks - they receive HTTP requests from external systems and execute configured handlers. Each webhook can have multiple handlers that process the incoming request data.
Handler Types:
agent- Execute an agent with the webhook dataaction-chain- Run an action chain workflow
Webhook Management
Create Webhook
POST /orgs/{organizationId}/webhooksRequired Role: Admin or Owner
Request Body:
{
"name": "External System Webhook",
"description": "Receives events from our CRM system"
}Response:
{
"webhookId": "webhook-123",
"organizationId": "org-456",
"name": "External System Webhook",
"description": "Receives events from our CRM system",
"creatorUserId": "user-789",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}List Webhooks
GET /orgs/{organizationId}/webhooksRequired Role: Admin or Owner
Query Parameters:
limit(optional): Number of results (default: 20)cursor(optional): Pagination offset
Response:
{
"items": [
{
"webhookId": "webhook-123",
"name": "External System Webhook",
"description": "Receives events from CRM",
"createdAt": "2024-01-01T00:00:00Z"
}
],
"totalRows": 1,
"offset": 0
}Get Webhook
GET /orgs/{organizationId}/webhooks/{webhookId}Required Role: Admin or Owner
Update Webhook
PUT /orgs/{organizationId}/webhooks/{webhookId}Required Role: Admin or Owner
Request Body:
{
"name": "Updated Webhook Name",
"description": "Updated description",
"signatureVerification": {
"header": "x-xero-signature",
"algorithm": "hmac-sha256",
"encoding": "base64",
"secretVaultItemId": "vault-item-id"
}
}signatureVerification is optional on create and update. Omit it to leave the current setting unchanged; send null to turn verification off. See Signature Verification.
Delete Webhook
DELETE /orgs/{organizationId}/webhooks/{webhookId}Required Role: Admin or Owner
Permanently deletes the webhook and all its handlers.
Executing Webhooks
Execute Webhook
POST /orgs/{organizationId}/webhooks/{webhookId}/executeAuthentication: None required (public endpoint)
Request Body: Any JSON payload - this data will be passed to the handlers
Example:
{
"event": "ticket_created",
"ticketId": "TICKET-123",
"priority": "high",
"customer": {
"name": "John Doe",
"email": "john@example.com"
},
"subject": "Unable to login",
"description": "Customer is getting error 500 when trying to login"
}Response: Empty object {}
The webhook executes all configured handlers asynchronously. The response is returned immediately without waiting for handler execution.
A workflow handler set to respond with its output is the exception: the sender waits for that tool workflow, and the reply has to arrive within 29 seconds. A tool workflow that runs longer should be started with POST /orgs/{organizationId}/workflows/run-stream/{workflowId} using an API key. That route answers with server-sent events: ping frames while the workflow runs, then one result frame carrying executionId, startDate and output, or one error frame carrying executionId and error. Read the body until one of those two frames arrives.
If a workflow handler’s mapped input is missing a field its workflow requires, the request is refused with 400 and { "message": "Trigger input is invalid: Missing required field: <name>" }. No handler runs, so the same request can be sent again once it is fixed. The organization’s owners, and the webhook’s creator while they are an admin, are notified. An empty string or null counts as missing.
Webhook Execution Context
Handlers receive a WebhookExecutionContext object containing:
{
body: {}, // Request body as JSON
headers: {}, // HTTP headers
method: "POST", // HTTP method
path: "/orgs/org-id/webhooks/webhook-id/execute", // Request path
queryParams: {}, // Query parameters
pathParams: {} // Path parameters
}Webhook Handlers
Handlers define what actions to take when a webhook is triggered.
Create Handler
POST /orgs/{organizationId}/webhooks/{webhookId}/handlersRequired Role: Admin or Owner
Agent Handler Example:
{
"handlerType": "agent",
"agentId": "agent-123",
"instructions": "Process the support ticket and create a response. Extract the ticket ID, priority, and customer information from the webhook data."
}Action Chain Handler Example:
{
"handlerType": "action-chain",
"actionChainId": "chain-456",
"instructions": "Process CRM event",
"inputMapping": {
"customerId": "{{body.customer.id}}",
"ticketNumber": "{{body.ticketId}}",
"priority": "{{body.priority}}"
}
}Response:
{
"handlerId": "handler-789",
"webhookId": "webhook-123",
"organizationId": "org-456",
"handlerType": "agent",
"agentId": "agent-123",
"agentName": "Support Agent",
"instructions": "Process the support ticket...",
"createdAt": "2024-01-01T00:00:00Z"
}Handler Types
Agent Handler
Executes an agent with the webhook data. A new conversation is created for each webhook execution.
Required Fields:
handlerType:"agent"agentId: ID of the agent to executeinstructions: Instructions for processing the webhook data
Behavior:
- Creates a new conversation
- Sends a message to the agent with webhook data and instructions
- Agent processes the data and can use its tools
- No user interaction possible (automated execution only)
Action Chain Handler
Runs an action chain workflow with mapped input data.
Required Fields:
handlerType:"action-chain"actionChainId: ID of the action chain to runinstructions: Description of the handler purposeinputMapping: Maps webhook data to chain input variables
Input Mapping:
Maps webhook context fields to action chain input variables using Handlebars syntax:
{
"inputMapping": {
"customerId": "{{body.customer.id}}",
"customerEmail": "{{body.customer.email}}",
"ticketId": "{{body.ticketId}}",
"priority": "{{body.priority}}",
"source": "{{headers.X-Source-System}}"
}
}Mapping Sources:
{{body.*}}- Request body fields{{headers.*}}- HTTP headers{{queryParams.*}}- Query parameters{{pathParams.*}}- Path parameters
List Handlers
GET /orgs/{organizationId}/webhooks/{webhookId}/handlersRequired Role: Admin or Owner
Query Parameters:
limit(optional): Number of results (default: 20)cursor(optional): Pagination offset
Get Handler
GET /orgs/{organizationId}/webhooks/{webhookId}/handlers/{handlerId}Required Role: Admin or Owner
Update Handler
PUT /orgs/{organizationId}/webhooks/{webhookId}/handlers/{handlerId}Required Role: Admin or Owner
Request Body:
{
"instructions": "Updated instructions",
"inputMapping": {
"customerId": "{{body.customer_id}}"
}
}Delete Handler
DELETE /orgs/{organizationId}/webhooks/{webhookId}/handlers/{handlerId}Required Role: Admin or Owner
Example Use Cases
CRM Integration
Scenario: When a support ticket is created in your CRM, trigger an Hutly agent to analyze it and suggest a response.
Setup:
- Create webhook:
{
"name": "CRM Support Tickets",
"description": "Receives new support ticket events"
}- Create agent handler:
{
"handlerType": "agent",
"agentId": "support-agent-123",
"instructions": "Analyze the support ticket from the CRM. Extract key information, categorize the issue, and draft a response for the support team. If the ticket is about billing, notify the billing team."
}- Configure your CRM to POST to:
https://api.intellia.com.au/orgs/your-org-id/webhooks/webhook-id/executeE-commerce Order Processing
Scenario: When an order is placed, run an action chain to process it.
Setup:
- Create webhook:
{
"name": "Order Events",
"description": "Receives order creation events from Shopify"
}- Create action chain handler:
{
"handlerType": "action-chain",
"actionChainId": "order-processing-chain",
"instructions": "Process new e-commerce order",
"inputMapping": {
"orderId": "{{body.id}}",
"customerEmail": "{{body.customer.email}}",
"orderTotal": "{{body.total_price}}",
"items": "{{body.line_items}}"
}
}Form Submissions
Scenario: Process form submissions from your website.
Setup:
- Create webhook and agent handler
- Add form submission endpoint to your website:
async function handleFormSubmit(formData) {
const response = await fetch(
'https://api.intellia.com.au/orgs/org-id/webhooks/webhook-id/execute',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
formType: 'contact',
name: formData.name,
email: formData.email,
message: formData.message,
submittedAt: new Date().toISOString()
})
}
);
}Security Considerations
Public Access
Turn on signature verification whenever the sender signs its requests.
Best Practices:
- Keep URLs Secret - Treat webhook URLs like API keys
- Validate Payload - Have handlers validate incoming data
- Use HTTPS - Always use HTTPS for webhook URLs
- Rotate URLs - If a webhook URL is compromised, delete and recreate
- Monitor Usage - Review webhook execution logs regularly
- Rate Limiting - Consider implementing rate limiting on your side
Signature Verification
A webhook can require every call to carry a signature of the raw request body, keyed with a secret you share with the sender. The check runs before response rules, handlers and the credit check. There are two schemes, chosen by algorithm: hmac-sha256, an HMAC-SHA256 of the body in a header you name, and stripe-v1, Stripe’s signature scheme (see the Stripe example below).
| Field | Meaning |
|---|---|
algorithm |
hmac-sha256 or stripe-v1 |
header |
hmac-sha256 only. Request header carrying the signature, e.g. x-xero-signature. Matched case-insensitively. |
encoding |
hmac-sha256 only. base64 or hex — how the sender encodes the signature |
secretVaultItemId |
A secret in this organization’s Password vault holding the signing key. hmac-sha256: null while none is linked; omitted on update keeps the linked one. |
prefix |
hmac-sha256 only, optional. Text before the signature in the header, e.g. sha256= |
moduleDeclared |
hmac-sha256 only, read-only. true when the installing module declared the check: you can link the secret, but not change or remove the check. |
| Request | Response |
|---|---|
| Signature matches | Handled as normal |
| Signature matches, but the org is out of credits or its office is paused | 200; nothing runs. The refusal is logged. |
| Header missing, body empty, or signature wrong | 401 Invalid signature; nothing runs |
| No secret linked | 401 Invalid signature; nothing runs |
stripe-v1 timestamp more than 5 minutes from now |
401 Invalid signature; nothing runs |
| Vault secret deleted or unreadable | 503; nothing runs |
The body must be JSON: a body that does not parse is refused with 400 before the signature is checked. Slack’s timestamped v0= scheme is not supported.
The signature covers the body only. Query parameters and headers other than the signature header are not authenticated, so map handler inputs and write rule conditions from body, not from queryParams or headers. hmac-sha256 has no replay protection: a captured signed request verifies every time it is resent and runs the handlers again. stripe-v1 signs a timestamp with the body and refuses requests more than 5 minutes old, so a captured request can only be replayed inside that window. Either way, use the sender’s event ids or sequence numbers to skip events you have already handled (Xero sends firstEventSequence and lastEventSequence).
Xero example. Store the webhook key from the Xero developer portal in the Password vault as a secret, then create or update the webhook with:
{
"name": "xero-invoices",
"description": "Xero invoice events",
"signatureVerification": {
"header": "x-xero-signature",
"algorithm": "hmac-sha256",
"encoding": "base64",
"secretVaultItemId": "<the secret's vault item id>"
}
}When you save the webhook in Xero, its intent-to-receive check sends one correctly signed request (expects 200) and unsigned or wrongly signed ones (expect 401), each within 5 seconds. A webhook with verification on passes. The probe’s body is an empty batch ({"events":[], …}); add a response rule with "condition": "!body.events[0]" and "skipHandlers": true if your handlers should not run for it.
Stripe example. Store the endpoint’s whsec_… signing secret from the Stripe Dashboard in the Password vault as a secret, then create or update the webhook with:
{
"name": "stripe-events",
"description": "Stripe account events",
"signatureVerification": {
"algorithm": "stripe-v1",
"secretVaultItemId": "<the secret's vault item id>"
}
}The signature is read from the Stripe-Signature header (t=<unix time>,v1=<hex>), computed over <t>.<raw body>; any of several v1 values may match. header, encoding and prefix are refused on a stripe-v1 config. A Stripe connection sets this up for you: its events webhook is created with stripe-v1 and the endpoint’s signing secret.
In the web app, the same settings are under Verify signatures on the webhook’s page; choose Stripe under Scheme for stripe-v1. From the CLI (version 1.15.0 or later; earlier versions do not know these flags or the signatureVerification YAML key), pass --signature-header, --signature-encoding, --signature-secret <vault secret name or id> and optionally --signature-prefix to intellia webhooks create or update, or --no-signature to turn it off. For Stripe (version 1.17.0 or later), pass --signature-scheme stripe-v1 --signature-secret <vault secret name or id>; in webhook YAML, signatureVerification: { scheme: stripe-v1, secret: <vault secret name or id> }.
Validation in Handlers
For agent handlers, include validation instructions:
{
"instructions": "IMPORTANT: First validate the webhook data. Check that required fields (ticketId, priority, customer.email) are present. If validation fails, log the error and do not proceed. Only process valid ticket data."
}Troubleshooting
Common Issues
| Issue | Solution |
|---|---|
| Webhook not executing | Verify webhook ID and organization ID in URL |
| Handler not running | Check handler exists and is properly configured |
| Agent handler fails | Review conversation logs for the created conversation |
| Action chain not starting | Verify action chain ID and input mapping |
| Invalid input mapping | Check field paths match webhook payload structure |
Testing Webhooks
Use curl or Postman to test webhook execution:
curl -X POST \
https://api.intellia.com.au/orgs/org-123/webhooks/webhook-456/execute \
-H 'Content-Type: application/json' \
-d '{
"test": true,
"message": "Testing webhook execution"
}'Debugging
- Check Handler Configuration - Verify agent/chain IDs are correct
- Review Input Mapping - Ensure paths match webhook payload
- Test Payload Structure - Send test requests with various payloads
- Monitor Conversations - For agent handlers, check created conversations
- Action Chain Logs - For chain handlers, review chain execution logs
Limitations
- Maximum 500 handlers per webhook
- Handler execution is asynchronous (no response from handlers)
- Public access only (no authentication on execute endpoint); turn on signature verification to refuse unsigned calls
- Agent handlers create new conversations (cannot append to existing)