Skip to Content

Receiving Callbacks

When actions complete processing, AgentPress sends signed HTTP callbacks to your configured endpoints. These callbacks contain the action result, including the agent’s text response and any tool calls made during processing.

Callbacks use the same Svix HMAC-SHA256 signing scheme as inbound webhooks, so your server can verify their authenticity using the @agentpress/sdk.

Setting Up Post-Events

Post-event callbacks are configured per action rule in the admin console.

  1. Navigate to Actions > Rules and select a rule
  2. Open the Post-Events tab
  3. Configure the callback endpoint

Endpoint Configuration

FieldDescription
URLThe endpoint AgentPress sends callbacks to
MethodHTTP method (POST)
HeadersCustom headers to include (e.g., authorization tokens)
Custom FieldsAdditional key-value pairs merged into the callback payload
TimeoutRequest timeout in seconds

Retry Configuration

FieldDefaultDescription
Max Attempts3Maximum delivery attempts
Initial Delay1sDelay before first retry
Backoff Multiplier2Exponential backoff factor

Verifying Callbacks

All callbacks include Svix signature headers (svix-id, svix-timestamp, svix-signature). You should always verify the signature before processing the payload.

constructEvent verifies the signature and parses the payload into a typed ActionCallbackPayload in a single step. This is the recommended approach.

Express:

import express from "express"; import { AgentPress, WebhookSignatureError, type ActionCallbackPayload, } from "@agentpress/sdk"; const app = express(); const client = new AgentPress({ webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET, }); app.post( "/webhooks/agentpress", express.raw({ type: "application/json" }), (req, res) => { let event: ActionCallbackPayload; try { event = client.webhooks.constructEvent({ payload: req.body, headers: { "svix-id": req.headers["svix-id"] as string, "svix-timestamp": req.headers["svix-timestamp"] as string, "svix-signature": req.headers["svix-signature"] as string, }, }); } catch (error) { if (error instanceof WebhookSignatureError) { return res.status(401).json({ error: "Invalid signature" }); } throw error; } console.log(`Action ${event.actionId} ${event.status}`); res.json({ received: true }); } );

Important: The request body must be the raw string or Buffer, not parsed JSON. Using express.json() instead of express.raw() will cause signature verification to fail because the re-serialized body may differ from the original signed payload.

Hono:

import { Hono } from "hono"; import { AgentPress, WebhookSignatureError } from "@agentpress/sdk"; const app = new Hono(); const client = new AgentPress({ webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET, }); app.post("/webhooks/agentpress", async (c) => { const rawBody = await c.req.text(); try { const event = client.webhooks.constructEvent({ payload: rawBody, headers: { "svix-id": c.req.header("svix-id")!, "svix-timestamp": c.req.header("svix-timestamp")!, "svix-signature": c.req.header("svix-signature")!, }, }); console.log(`Action ${event.actionId} ${event.status}`); return c.json({ received: true }); } catch (error) { if (error instanceof WebhookSignatureError) { return c.json({ error: "Invalid signature" }, 401); } throw error; } });

Manual Verification

If you need to separate signature verification from payload parsing, the SDK provides two lower-level methods.

verify returns a boolean:

const isValid = client.webhooks.verify({ payload: rawBody, headers: { "svix-id": headers["svix-id"], "svix-timestamp": headers["svix-timestamp"], "svix-signature": headers["svix-signature"], }, }); if (!isValid) { return new Response("Unauthorized", { status: 401 }); } const event = JSON.parse(rawBody) as ActionCallbackPayload;

verifyOrThrow throws WebhookSignatureError on failure:

try { client.webhooks.verifyOrThrow({ payload: rawBody, headers: { "svix-id": headers["svix-id"], "svix-timestamp": headers["svix-timestamp"], "svix-signature": headers["svix-signature"], }, }); } catch (error) { if (error instanceof WebhookSignatureError) { return new Response("Invalid signature", { status: 401 }); } } const event = JSON.parse(rawBody) as ActionCallbackPayload;

Verification Details

  • Timestamp must be within 5 minutes of current time (replay protection)
  • Uses timing-safe comparison to prevent timing attacks
  • Supports multiple space-separated signatures in the svix-signature header

Callback Payload

The ActionCallbackPayload type describes the shape of callback data your endpoint receives.

Fields

FieldTypeDescription
actionIdstringUnique action identifier
statusActionStatusAction outcome
actionTypestringThe action type (e.g., "review.created")
completedAtstringISO 8601 completion timestamp
sourceDataRecord<string, unknown>Original data from the inbound webhook
externalIdstring | nullExternal system identifier
userIdstring | nullResolved AgentPress user ID
threadIdstring | nullThread ID if a conversation was created
agentResponseAgentResponseAgent’s text response and tool calls
errorMessagestring | nullError details if the action failed
rejectionReasonstring | nullReason if rejected by a human reviewer

ActionStatus Values

StatusDescription
pendingAction created, awaiting processing
stagedAgent processed, awaiting human review
approvedHuman approved the staged response
rejectedHuman rejected the staged response
completedAction fully completed
failedAction processing failed
expiredAction expired before processing

AgentResponse Structure

interface AgentResponse { text: string | null; toolCalls: ToolCallResult[]; } interface ToolCallResult { toolName: string; arguments: Record<string, unknown>; result: unknown; }

Example Callback

{ "actionId": "550e8400-e29b-41d4-a716-446655440000", "status": "completed", "actionType": "review.created", "completedAt": "2025-01-16T18:30:00.000Z", "sourceData": { "reviewText": "Great service!", "rating": 5, "locationName": "Downtown Location", "reviewerName": "Jane Smith" }, "externalId": "review-12345", "userId": "user-uuid-here", "threadId": "thread-uuid-here", "agentResponse": { "text": "Thank you for your wonderful review, Jane! We're thrilled to hear about your experience at our Downtown Location.", "toolCalls": [ { "toolName": "postReviewResponse", "arguments": { "reviewId": "review-12345", "response": "..." }, "result": { "success": true } } ] }, "errorMessage": null, "rejectionReason": null }

Retry Behavior

When a callback delivery fails, AgentPress retries with exponential backoff based on your post-event configuration.

ResponseBehavior
2xxDelivery successful, no retry
4xxPermanent failure, no retry (fix your endpoint)
5xxTemporary failure, retry with backoff
TimeoutRetry with backoff

After exhausting all retry attempts, the callback is marked as failed. You can view delivery status in the Actions console.

Full Example

A complete Express server that receives and processes action callbacks:

import express from "express"; import { AgentPress, WebhookSignatureError, type ActionCallbackPayload, } from "@agentpress/sdk"; const app = express(); const client = new AgentPress({ webhookSecret: process.env.AGENTPRESS_WEBHOOK_SECRET, }); app.post( "/webhooks/agentpress", express.raw({ type: "application/json" }), (req, res) => { let event: ActionCallbackPayload; try { event = client.webhooks.constructEvent({ payload: req.body, headers: { "svix-id": req.headers["svix-id"] as string, "svix-timestamp": req.headers["svix-timestamp"] as string, "svix-signature": req.headers["svix-signature"] as string, }, }); } catch (error) { if (error instanceof WebhookSignatureError) { console.error("Invalid webhook signature"); return res.status(401).json({ error: "Invalid signature" }); } console.error("Webhook processing error:", error); return res.status(400).json({ error: "Invalid payload" }); } switch (event.status) { case "completed": console.log(`Action ${event.actionId} completed`); if (event.agentResponse.text) { console.log("Agent response:", event.agentResponse.text); } for (const call of event.agentResponse.toolCalls) { console.log(`Tool ${call.toolName} called with`, call.arguments); } break; case "failed": console.error( `Action ${event.actionId} failed: ${event.errorMessage}` ); break; case "rejected": console.log( `Action ${event.actionId} rejected: ${event.rejectionReason}` ); break; default: console.log(`Action ${event.actionId} status: ${event.status}`); } res.json({ received: true }); } ); app.listen(3000, () => { console.log("Webhook server listening on port 3000"); });

Key points:

  • Always return a 2xx response promptly to acknowledge receipt. Slow responses may trigger retries.
  • Process callbacks asynchronously if your handler involves heavy computation.
  • Log the actionId for traceability across your systems.
  • Handle all status values, not just "completed" — actions can also be "failed", "rejected", or "staged".
Last updated on