Webhooks
Get notified instantly when a form is submitted. AgentForms sends a signed POST request to your callback_url with the full response data.
Setting Up Webhooks
Pass a callback_url when creating a form. Optionally include callback_metadata to attach context (e.g. conversation ID, task ID) that will be echoed back in the webhook payload.
form = client.create_form( title="Approval Request", fields=[...], callback_url="https://your-app.com/webhooks/agentforms", callback_metadata={ "conversation_id": "conv_abc123", "task": "approval", }, )
A webhook secret (64-character hex string) is auto-generated by the server whenever you provide a callback_url. This secret is used to sign the payload but is never exposed via the API.
Webhook Payload
When a form is submitted, AgentForms sends a POST request to your callback_url with this JSON body:
{
"event": "form.completed",
"form_id": "01JNXXXXXXXXXXXXXXXXXX",
"form_title": "Approval Request",
"completed_at": "2026-03-05T12:34:56Z",
"response_id": "01JNYYYYYYYYYYYYYYY",
"data": {
"approved": true,
"comment": "Looks good to me"
},
"metadata": {
"ip": "203.0.113.42",
"user_agent": "Mozilla/5.0..."
},
"callback_metadata": {
"conversation_id": "conv_abc123",
"task": "approval"
}
}| Field | Type | Description |
|---|---|---|
event | string | Always "form.completed" |
form_id | string | ULID of the form |
form_title | string | Form title |
completed_at | string | ISO 8601 timestamp |
response_id | string | ULID of the response |
data | object | Field key → submitted value |
metadata | object | Submitter IP and user agent |
callback_metadata | object | Your custom metadata (pass-through from form creation) |
Request Details
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 10 seconds (configurable via AGENTFORMS_WEBHOOK_TIMEOUT) |
Signature Verification
Every webhook request includes an HMAC-SHA256 signature in the X-AgentForms-Signature header:
X-AgentForms-Signature: sha256=a1b2c3d4e5f6...
Verify this signature to ensure the request came from AgentForms and wasn't tampered with.
Python
import hashlib, hmac, json def verify_signature(body: bytes, secret: str, signature_header: str) -> bool: expected = hmac.new( secret.encode(), body, hashlib.sha256, ).hexdigest() return hmac.compare_digest( f"sha256={expected}", signature_header, ) # In your webhook handler: if not verify_signature( request.body, WEBHOOK_SECRET, request.headers["X-AgentForms-Signature"], ): raise ValueError("Invalid signature")
Node.js
const crypto = require("crypto"); function verifySignature(body, secret, signatureHeader) { const expected = crypto .createHmac("sha256", secret) .update(body) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(`sha256=${expected}`), Buffer.from(signatureHeader), ); } // In your Express handler: app.post("/webhooks/agentforms", (req, res) => { const sig = req.headers["x-agentforms-signature"]; if (!verifySignature(req.rawBody, WEBHOOK_SECRET, sig)) { return res.status(401).send("Invalid signature"); } const payload = JSON.parse(req.rawBody); // Handle the webhook... res.sendStatus(200); });
Retry Behavior
If your endpoint returns a non-2xx status code or the request times out, AgentForms retries with exponential backoff:
| Attempt | Delay Before |
|---|---|
| 1st (initial) | Immediate |
| 2nd (retry 1) | 1 second |
| 3rd (retry 2) | 5 seconds |
| 4th (retry 3) | 25 seconds |
After all retries are exhausted, the webhook delivery is abandoned. The response data is still available via the List Responses API endpoint.
Tip: Always return a 200 status quickly from your webhook handler. Do any heavy processing asynchronously to avoid timeouts.
Configuration
Webhook behavior can be tuned with environment variables on the AgentForms server:
| Variable | Default | Description |
|---|---|---|
AGENTFORMS_WEBHOOK_TIMEOUT | 10 | HTTP timeout in seconds for webhook delivery |
AGENTFORMS_WEBHOOK_MAX_RETRIES | 3 | Maximum number of retry attempts |