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.
- Navigate to Actions > Rules and select a rule
- Open the Post-Events tab
- Configure the callback endpoint
Endpoint Configuration
| Field | Description |
|---|---|
| URL | The endpoint AgentPress sends callbacks to |
| Method | HTTP method (POST) |
| Headers | Custom headers to include (e.g., authorization tokens) |
| Custom Fields | Additional key-value pairs merged into the callback payload |
| Timeout | Request timeout in seconds |
Retry Configuration
| Field | Default | Description |
|---|---|---|
| Max Attempts | 3 | Maximum delivery attempts |
| Initial Delay | 1s | Delay before first retry |
| Backoff Multiplier | 2 | Exponential 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.
Using constructEvent (Recommended)
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 ofexpress.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-signatureheader
Callback Payload
The ActionCallbackPayload type describes the shape of callback data your endpoint receives.
Fields
| Field | Type | Description |
|---|---|---|
actionId | string | Unique action identifier |
status | ActionStatus | Action outcome |
actionType | string | The action type (e.g., "review.created") |
completedAt | string | ISO 8601 completion timestamp |
sourceData | Record<string, unknown> | Original data from the inbound webhook |
externalId | string | null | External system identifier |
userId | string | null | Resolved AgentPress user ID |
threadId | string | null | Thread ID if a conversation was created |
agentResponse | AgentResponse | Agent’s text response and tool calls |
errorMessage | string | null | Error details if the action failed |
rejectionReason | string | null | Reason if rejected by a human reviewer |
ActionStatus Values
| Status | Description |
|---|---|
pending | Action created, awaiting processing |
staged | Agent processed, awaiting human review |
approved | Human approved the staged response |
rejected | Human rejected the staged response |
completed | Action fully completed |
failed | Action processing failed |
expired | Action 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.
| Response | Behavior |
|---|---|
| 2xx | Delivery successful, no retry |
| 4xx | Permanent failure, no retry (fix your endpoint) |
| 5xx | Temporary failure, retry with backoff |
| Timeout | Retry 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
actionIdfor traceability across your systems. - Handle all status values, not just
"completed"— actions can also be"failed","rejected", or"staged".