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.
How It Works
When an external event occurs in your system, AgentPress processes it through a structured pipeline:
1. External Event
Your system detects an event (e.g., new review, form submission, CRM update)
2. Send Signed Webhook
Your server (or the SDK) sends a signed HTTP POST to AgentPress
with the event payload and Svix signature headers
3. AgentPress Creates Action
The webhook is verified, deduplicated, and stored as a
pending action linked to the matching action rule
4. Agent Processes Action
The configured agent picks up the pending action, executes
its instructions, and pauses if a tool requires approval
5. Post-Event Callbacks (optional)
AgentPress sends signed lifecycle callbacks to your configured
endpoint for pending approval, approved/running, and terminal events
6. Your Server Handles Lifecycle Events
Your server verifies the callback signature, shows approval requests,
and reacts to completed, failed, rejected, or expired actionsFor the recommended approach using the
@agentpress/sdkpackage, see Using the SDK.
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., "My Service Reviews") |
| Webhook Identifier | URL-safe identifier used in the endpoint (e.g., my_webhook) |
| Run As (User) | Default user for actions created by this webhook. Optional — if left empty, the webhook payload must include userId (and authProvider for external users). |
| 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;
// Optional: User to run the action as.
// If provided with authProvider, resolves via external auth.
// If a valid UUID without authProvider, resolves via direct lookup.
// If omitted, falls back to the webhook's configured default user.
userId?: string;
// Optional: External auth provider name (e.g., "google_business").
// Required when userId is an external ID (not a UUID).
authProvider?: string;
// Optional: Override the default action rule
actionRuleId?: string;
// Optional: Additional per-request instructions for the agent
instructions?: string;
// Custom data specific to your integration
data: {
[key: string]: unknown;
};
}User Resolution
When a webhook is received, AgentPress resolves which user the action runs as by checking three sources in order:
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
Manual Test Script
Create a file called test-webhook.js:
const crypto = require("crypto");
const ORG_ID = "your-org-id-here";
const WEBHOOK_IDENTIFIER = "my_webhook";
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.jsResponse 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 |
422 | No user resolved — neither the payload nor the webhook config provided a valid user |
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"
}No User Resolved (422)
{
"error": "No user resolved — provide userId (+ authProvider) in the webhook payload, or configure a default user on the webhook in the console."
}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
Post-Event Callbacks
AgentPress can send signed HTTP callbacks as actions move through their lifecycle. A post-event callback can subscribe to any mix of these public event types:
| Event Type | Meaning |
|---|---|
action.pending_approval | A tool call is staged and needs approval. |
action.approved | The action was approved or is currently executing. Not terminal. |
action.completed | The action finished successfully. |
action.failed | The action failed during processing or execution. |
action.rejected | The staged action was rejected. |
action.expired | The staged action expired before approval. |
Callbacks use the same Svix HMAC signing scheme as incoming webhooks, so your server can verify their authenticity using the same approach. Callback payloads include eventType, current status, occurredAt, optional stagedToolCall, and optional resultSummary.
Treat approved and executing as in-flight states. Only completed, failed, rejected, and expired are terminal.
For setup details, SDK-based signature verification, and payload examples, see Receiving Callbacks.
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"
"No user resolved" (422)
A user must be resolved from at least one source:
- Include
userId(+authProviderfor external IDs) in the webhook payload, or - Configure a default user on the webhook via Actions → Webhooks → click your webhook → Run As section
If neither is provided, the webhook returns 422 and no action is created.
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)