Skip to content

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 data
  • action-chain - Run an action chain workflow
Webhook execution endpoints are publicly accessible (no authentication required). Use this for integrating with third-party services that need to trigger actions in Hutly.

Webhook Management

Create Webhook

POST /orgs/{organizationId}/webhooks

Required 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}/webhooks

Required 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}/execute

Authentication: 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}/handlers

Required 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 execute
  • instructions: 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 run
  • instructions: Description of the handler purpose
  • inputMapping: 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}/handlers

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

  1. Create webhook:
{
  "name": "CRM Support Tickets",
  "description": "Receives new support ticket events"
}
  1. 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."
}
  1. Configure your CRM to POST to:
https://api.intellia.com.au/orgs/your-org-id/webhooks/webhook-id/execute

E-commerce Order Processing

Scenario: When an order is placed, run an action chain to process it.

Setup:

  1. Create webhook:
{
  "name": "Order Events",
  "description": "Receives order creation events from Shopify"
}
  1. 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:

  1. Create webhook and agent handler
  2. 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

Webhook execution endpoints are publicly accessible without authentication. Anyone with the webhook URL can trigger it.

Turn on signature verification whenever the sender signs its requests.

Best Practices:

  1. Keep URLs Secret - Treat webhook URLs like API keys
  2. Validate Payload - Have handlers validate incoming data
  3. Use HTTPS - Always use HTTPS for webhook URLs
  4. Rotate URLs - If a webhook URL is compromised, delete and recreate
  5. Monitor Usage - Review webhook execution logs regularly
  6. 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

  1. Check Handler Configuration - Verify agent/chain IDs are correct
  2. Review Input Mapping - Ensure paths match webhook payload
  3. Test Payload Structure - Send test requests with various payloads
  4. Monitor Conversations - For agent handlers, check created conversations
  5. 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)