Skip to content

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.

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 runs any assigned tools

5. Post-Event Callback (optional)
   When processing completes, AgentPress sends a signed callback
   to your configured endpoint with the result

6. Your Server Handles Result
   Your server verifies the callback signature and processes
   the agent's output (e.g., post a reply, update a record)

For the recommended approach using the @agentpress/sdk package, see Using the SDK.

The webhook system uses Svix for HMAC signature verification, ensuring secure communication between external systems and AgentPress.

Endpoint Format: POST /webhooks/actions/:orgId/:webhookIdentifier

In the AgentPress Admin Console:

  1. Navigate to ActionsWebhooks tab
  2. Click Create Webhook
  3. Fill in the configuration:
FieldDescription
NameDisplay name for the webhook (e.g., “My Service Reviews”)
Webhook IdentifierURL-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 RuleRule to use when processing incoming webhooks (optional)
  1. Click Create
  2. 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}

Before webhooks can create actions, you need at least one rule:

  1. Navigate to ActionsRules tab
  2. Click Create Rule
  3. Configure the rule:
FieldDescription
NameDisplay name for the rule
Action TypeType of action (e.g., review_response, email_reply)
AgentWhich agent handles these actions
InstructionsAdditional instructions appended to the agent prompt
Tools Requiring ApprovalTools that need human approval before execution
  1. Save the rule
  2. Link it to your webhook as the default rule

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;
  };
}

When a webhook is received, AgentPress resolves which user the action runs as by checking three sources in order:

flowchart TD
    A["Webhook received"] --> B{"Payload has<br/>`userId` + `authProvider`?"}
    B -- Yes --> C["Resolve via external auth provider"]
    C --> C1{"User found?"}
    C1 -- Yes --> OK["✅ Action created with resolved user"]
    C1 -- No --> FAIL["❌ 422 — user not found for given authProvider"]

    B -- No --> D{"Payload has<br/>`userId` (valid UUID)?"}
    D -- Yes --> E["Direct internal user lookup"]
    E --> E1{"User found?"}
    E1 -- Yes --> OK
    E1 -- No --> FAIL2["❌ 422 — no user with this ID"]

    D -- No --> F{"Webhook has<br/>default user configured?"}
    F -- Yes --> G["Use webhook's default user"]
    G --> G1{"User found?"}
    G1 -- Yes --> OK
    G1 -- No --> FAIL3["❌ 422 — configured user no longer exists"]

    F -- No --> FAIL4["❌ 422 — no user resolved from any source"]

    style OK fill:#166534,color:#fff
    style FAIL fill:#991b1b,color:#fff
    style FAIL2 fill:#991b1b,color:#fff
    style FAIL3 fill:#991b1b,color:#fff
    style FAIL4 fill:#991b1b,color:#fff
{
  "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"
  }
}

All webhook requests must include Svix signature headers:

HeaderDescription
svix-idUnique message ID (e.g., msg_abc123def456)
svix-timestampUnix timestamp in seconds
svix-signatureHMAC signature in format: v1,{base64_signature}

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.js
CodeDescription
200Success - action created or already exists
401Missing or invalid signature headers
403Webhook is disabled
404Webhook configuration not found
422No user resolved — neither the payload nor the webhook config provided a valid user
429Rate limit exceeded (100 requests/minute by default)
500Internal server error
{
  "success": true,
  "actionId": "uuid-of-created-action"
}

Idempotent Response (Action Already Exists)

Section titled “Idempotent Response (Action Already Exists)”
{
  "success": true,
  "actionId": "uuid-of-existing-action",
  "alreadyExists": true
}
{
  "success": true,
  "skipped": true,
  "reason": "No action rule configured"
}
{
  "error": "No user resolved — provide userId (+ authProvider) in the webhook payload, or configure a default user on the webhook in the console."
}

After sending a webhook:

  1. Navigate to ActionsPending tab in the console
  2. You should see your new action with status “Pending”
  3. Click on the action to view the source data

When actions complete processing, AgentPress can send signed HTTP callbacks to your configured endpoints with the result. This allows your system to react to the agent’s output — for example, posting a generated reply, updating a CRM record, or triggering a downstream workflow.

Callbacks use the same Svix HMAC signing scheme as incoming webhooks, so your server can verify their authenticity using the same approach.

For setup details and SDK-based signature verification, see Receiving Callbacks.

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 3001

Use the ngrok URL as your webhook endpoint:

https://abc123.ngrok.io/webhooks/actions/{orgId}/{webhookIdentifier}
  • 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 externalId values are detected and won’t create duplicate actions
  • Encryption: Webhook secrets are encrypted at rest

Ensure all three Svix headers are present in your request:

  • svix-id
  • svix-timestamp
  • svix-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)
  • Verify the orgId in the URL matches your organization
  • Verify the webhookIdentifier matches what you configured

Enable the webhook in ActionsWebhooks → click on your webhook → toggle “Enabled"

A user must be resolved from at least one source:

  • Include userId (+ authProvider for external IDs) in the webhook payload, or
  • Configure a default user on the webhook via ActionsWebhooks → click your webhook → Run As section

If neither is provided, the webhook returns 422 and no action is created.

  • 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

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)