Actions Webhooks
AgentPress supports incoming webhooks from external integrations to create pending actions for agent processing. This enables automation workflows where external systems (like review platforms, CRMs, or custom integrations) can trigger agent actions.
Overview
The webhook system uses Svix for HMAC signature verification, ensuring secure communication between external systems and AgentPress.
Endpoint Format: POST /webhooks/actions/:orgId/:webhookIdentifier
Setting Up Webhooks
Step 1: Create a Webhook Configuration
In the AgentPress Admin Console:
- Navigate to Actions → Webhooks tab
- Click Create Webhook
- Fill in the configuration:
| Field | Description |
|---|---|
| Name | Display name for the webhook (e.g., “Local Falcon Reviews”) |
| Webhook Identifier | URL-safe identifier used in the endpoint (e.g., local_falcon) |
| Default Action Rule | Rule to use when processing incoming webhooks (optional) |
- Click Create
- Important: Copy and save the webhook secret from the dialog - it’s only shown once!
Your webhook URL will be:
https://api.agent.press/webhooks/actions/{orgId}/{webhookIdentifier}Step 2: Create an Action Rule
Before webhooks can create actions, you need at least one rule:
- Navigate to Actions → Rules tab
- Click Create Rule
- Configure the rule:
| Field | Description |
|---|---|
| Name | Display name for the rule |
| Action Type | Type of action (e.g., review_response, email_reply) |
| Agent | Which agent handles these actions |
| Instructions | Additional instructions appended to the agent prompt |
| Tools Requiring Approval | Tools that need human approval before execution |
- Save the rule
- Link it to your webhook as the default rule
Webhook Payload Structure
All webhooks must send a JSON payload with the following structure:
interface WebhookPayload {
// Event type identifier (e.g., "review.created", "email.received")
eventType: string;
// Unique ID from the external system (used for idempotency)
externalId: string;
// External user ID (resolved via auth provider if configured)
userId: string;
// Optional: Override the default action rule
actionRuleId?: string;
// Custom data specific to your integration
data: {
[key: string]: unknown;
};
}Example Payload
{
"eventType": "review.created",
"externalId": "review-12345",
"userId": "external-user-456",
"data": {
"reviewText": "Great service! The staff was super helpful.",
"rating": 5,
"locationName": "Downtown Location",
"reviewerName": "Jane Smith",
"reviewDate": "2025-01-16T17:30:00Z"
}
}Required Headers
All webhook requests must include Svix signature headers:
| Header | Description |
|---|---|
svix-id | Unique message ID (e.g., msg_abc123def456) |
svix-timestamp | Unix timestamp in seconds |
svix-signature | HMAC signature in format: v1,{base64_signature} |
Sending Test Webhooks
Option A: Using the Svix CLI (Recommended)
# Install svix CLI
npm install -g svix
# Send a test webhook
svix webhook test \
--url "http://localhost:3001/webhooks/actions/{YOUR_ORG_ID}/local_falcon" \
--secret "whsec_YOUR_WEBHOOK_SECRET" \
--data '{
"eventType": "review.created",
"externalId": "review-123",
"userId": "external-user-456",
"data": {
"reviewText": "Great service!",
"rating": 5,
"locationName": "Main Street Store",
"reviewerName": "John Doe"
}
}'Option B: Node.js Test Script
Create a file called test-webhook.js:
const crypto = require('crypto');
const ORG_ID = 'your-org-id-here';
const WEBHOOK_IDENTIFIER = 'local_falcon';
const SECRET = 'your-secret-here'; // Base64 part after whsec_
const API_URL = 'http://localhost:3001';
async function sendTestWebhook() {
const msgId = 'msg_' + crypto.randomBytes(16).toString('hex');
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = {
eventType: 'review.created',
externalId: `review-${Date.now()}`, // Unique for idempotency
userId: 'test-user-123',
data: {
reviewText: 'This place is amazing! The staff was super helpful.',
rating: 5,
locationName: 'Downtown Location',
reviewerName: 'Jane Smith',
reviewDate: new Date().toISOString()
}
};
const payloadString = JSON.stringify(payload);
const toSign = `${msgId}.${timestamp}.${payloadString}`;
const signature = crypto
.createHmac('sha256', Buffer.from(SECRET, 'base64'))
.update(toSign)
.digest('base64');
const response = await fetch(
`${API_URL}/webhooks/actions/${ORG_ID}/${WEBHOOK_IDENTIFIER}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'svix-id': msgId,
'svix-timestamp': timestamp,
'svix-signature': `v1,${signature}`
},
body: payloadString
}
);
const result = await response.json();
console.log('Status:', response.status);
console.log('Response:', result);
}
sendTestWebhook().catch(console.error);Run with:
node test-webhook.jsOption C: cURL with Manual Signing
# Generate signature using Node.js
node -e "
const crypto = require('crypto');
const secret = 'YOUR_WEBHOOK_SECRET'; // without 'whsec_' prefix
const msgId = 'msg_' + crypto.randomBytes(16).toString('hex');
const timestamp = Math.floor(Date.now() / 1000).toString();
const payload = JSON.stringify({
eventType: 'review.created',
externalId: 'review-123',
userId: 'external-user-456',
data: {
reviewText: 'Great service!',
rating: 5,
locationName: 'Main Street Store'
}
});
const toSign = msgId + '.' + timestamp + '.' + payload;
const signature = crypto
.createHmac('sha256', Buffer.from(secret, 'base64'))
.update(toSign)
.digest('base64');
console.log('svix-id:', msgId);
console.log('svix-timestamp:', timestamp);
console.log('svix-signature:', 'v1,' + signature);
console.log('payload:', payload);
"
# Use the generated values in cURL
curl -X POST "http://localhost:3001/webhooks/actions/{YOUR_ORG_ID}/local_falcon" \
-H "Content-Type: application/json" \
-H "svix-id: {GENERATED_MSG_ID}" \
-H "svix-timestamp: {GENERATED_TIMESTAMP}" \
-H "svix-signature: {GENERATED_SIGNATURE}" \
-d '{YOUR_PAYLOAD}'Response Codes
| Code | Description |
|---|---|
200 | Success - action created or already exists |
401 | Missing or invalid signature headers |
403 | Webhook is disabled |
404 | Webhook configuration not found |
429 | Rate limit exceeded (100 requests/minute by default) |
500 | Internal server error |
Success Response
{
"success": true,
"actionId": "uuid-of-created-action"
}Idempotent Response (Action Already Exists)
{
"success": true,
"actionId": "uuid-of-existing-action",
"alreadyExists": true
}Skipped Response (No Rule Configured)
{
"success": true,
"skipped": true,
"reason": "No action rule configured"
}Verifying Actions Were Created
After sending a webhook:
- Navigate to Actions → Pending tab in the console
- You should see your new action with status “Pending”
- Click on the action to view the source data
Local Development
Since webhooks require a public URL, use ngrok for local development:
# Terminal 1: Start your API server
bun dev
# Terminal 2: Create a tunnel to your API port
ngrok http 3001Use the ngrok URL as your webhook endpoint:
https://abc123.ngrok.io/webhooks/actions/{orgId}/{webhookIdentifier}Security Considerations
- Signature Verification: All webhooks are verified using Svix HMAC signatures
- Secret Rotation: Use the “Rotate Secret” button in the webhook settings to generate a new secret
- Rate Limiting: Webhooks are rate-limited to 100 requests per minute per IP by default
- Idempotency: Duplicate
externalIdvalues are detected and won’t create duplicate actions - Encryption: Webhook secrets are encrypted at rest
Troubleshooting
”Missing webhook signature headers”
Ensure all three Svix headers are present in your request:
svix-idsvix-timestampsvix-signature
”Invalid signature”
- Verify your webhook secret is correct
- Ensure the secret is properly base64 encoded when generating the signature
- Check that the timestamp is recent (within 5 minutes)
“Webhook not configured”
- Verify the
orgIdin the URL matches your organization - Verify the
webhookIdentifiermatches what you configured
”Webhook is disabled”
Enable the webhook in Actions → Webhooks → click on your webhook → toggle “Enabled”
Action Not Appearing
- Check if a rule is configured and enabled
- Verify the rule is linked to the webhook as the default rule
- Check API logs for any processing errors
Rate Limit Exceeded
Wait 60 seconds before sending more requests, or configure higher limits via environment variables:
WEBHOOK_RATE_LIMIT_WINDOW_MS(default: 60000)WEBHOOK_RATE_LIMIT_MAX_REQUESTS(default: 100)