Webhooks
RobotRock can notify your application when a task is handled via webhooks.
Configure webhook on the client
Set webhook on createClient in your shared client module. It applies to every action when you call sendToHuman:
// lib/robotrock.ts
import { createClient } from "robotrock";
export const robotrock = createClient({
app: "my-service",
webhook: {
url: "https://your-app.com/api/robotrock/webhook",
headers: {
// place your headers here
},
},
});import { robotrock } from "@/lib/robotrock";
const response = await robotrock.sendToHuman({
type: "approval",
name: "Budget Approval",
actions: [
{ id: "approve", title: "Approve" },
{ id: "reject", title: "Reject" },
],
});headers is optional and defaults to {}.
Set app on createClient to control dashboard inbox grouping. When omitted, tasks use your API key name.
Webhook Payload
When a task is handled, RobotRock sends a POST request to your webhook URL:
{
"taskId": "task_123abc",
"action": {
"id": "approve",
"title": "Approve",
"data": {
"notes": "Looks good!"
}
},
"handledBy": "user@example.com",
"handledAt": "2024-01-15T10:30:00Z",
"handlerType": "webhook"
}Platform action IDs
Reviewers can close a task from the inbox without choosing one of your defined actions. These use reserved action.id values in the same webhook payload shape:
| Action ID | When used | action.data |
|---|---|---|
robotrock:mark-done | Manually marked as done in the inbox | {} |
robotrock:reject-request | Request rejected (bad agent output, loop, etc.) | { "feedback": "..." } (required) |
Webhooks fire when handlers are configured on the task (same URLs as your normal actions). Poll-only clients can branch on handled.action.id from getTask().
if (payload.action.id === "robotrock:reject-request") {
const { feedback } = payload.action.data as { feedback: string };
// stop the agent run, log the rejection, etc.
}Use the SDK helpers instead of string literals:
import {
isPlatformTerminalAction,
isPlatformRejectRequestAction,
parsePlatformRejectRequestData,
assertNotPlatformRejectRequest,
} from "robotrock";
if (isPlatformRejectRequestAction(payload.action.id)) {
const { feedback } = parsePlatformRejectRequestData(payload.action.data) ?? { feedback: "" };
// stop — do not continue the agent workflow
}
assertNotPlatformRejectRequest(payload.action.id, payload.action.data);
if (isPlatformTerminalAction(payload.action.id)) {
return Response.json({ ok: true, stopped: true });
}Agents must always check handled.action.id (polling, webhooks, MCP get_task) for these ids and stop — they are not your task's approve / reject actions.
Next.js API route example
Use a Route Handler in your Next.js app to receive webhook events:
// app/api/robotrock/webhook/route.ts
import { NextResponse } from "next/server";
import {
verifyRobotRockWebhook,
RobotRockWebhookError,
type RobotRockWebhookPayload,
} from "robotrock";
export async function POST(req: Request) {
let payload: RobotRockWebhookPayload;
try {
payload = await verifyRobotRockWebhook(req);
} catch (error) {
if (error instanceof RobotRockWebhookError) {
// Use `error.code` in your own audit logs and monitoring.
return NextResponse.json({ error: error.code }, { status: 401 });
}
return NextResponse.json({ error: "Unknown error" }, { status: 500 });
}
if (payload.action.id === "approve") {
// TODO: run your approved flow
} else if (payload.action.id === "reject") {
// TODO: run your rejection flow
}
// Request headers are included in payload.headers
console.log("RobotRock request id:", payload.headers["x-request-id"]);
return NextResponse.json({ ok: true });
}Audit Trail
RobotRock records webhook delivery in your workspace audit log (/[tenant]/audit). Three event types cover the full delivery lifecycle:
Handler run (handler_executed)
Logged for every delivery attempt (success or failure):
- Handler type (webhook)
- URL called
- HTTP status code
- Success/failure status
- Attempt number (e.g. attempt 2/8)
- Error message (if failed)
- Response time in milliseconds
Retry scheduled (handler_retry_scheduled)
Logged after a retriable failure when another attempt is queued:
- Next attempt number (e.g. attempt 3/8)
- Scheduled time for the next delivery
- URL and error from the failed attempt
Delivery failed (handler_delivery_failed)
Logged when RobotRock stops retrying:
- All attempts exhausted — 8 attempts failed (network error, 5xx, 408, or 429)
- Non-retriable response — your endpoint returned 4xx (except 408/429), so retries stopped immediately
Example timeline when a webhook endpoint is temporarily down:
- Handler run — Failed (503) · attempt 1/8
- Retry scheduled — attempt 2/8 at Mar 21, 3:41 PM
- Handler run — Failed (503) · attempt 2/8
- Retry scheduled — attempt 3/8 at Mar 21, 3:46 PM
- …continues until success or all 8 attempts fail…
- Delivery failed — All 8 attempts failed
Retries
When your webhook endpoint is unreachable or returns a retriable error, RobotRock retries delivery automatically.
Retry schedule
RobotRock makes up to 8 delivery attempts per handler. Delays between failed attempts:
| After attempt | Wait before next |
|---|---|
| 1 (immediate) | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 1 hour |
| 5 | 6 hours |
| 6 | 24 hours |
| 7 | 48 hours |
| 8 | give up |
Total retry window: up to ~80 hours from the first attempt.
Delivery headers
Each POST includes:
x-robotrock-signature— HMAC signature (same payload on every retry)x-robotrock-delivery-id— stable ID for this delivery chain (use for deduplication)x-robotrock-delivery-attempt— attempt number (1through8)
The JSON body and signature are identical across retries so verifyRobotRockWebhook() keeps working.
Idempotency
Your webhook handler may receive the same event more than once. Use x-robotrock-delivery-id together with taskId to deduplicate:
const deliveryId = request.headers.get("x-robotrock-delivery-id");
const alreadyProcessed = await db.webhookDeliveries.findUnique({
where: { deliveryId },
});
if (alreadyProcessed) {
return NextResponse.json({ ok: true, deduplicated: true });
}
// ... handle event, then record deliveryIdReturn 2xx quickly. Long-running work should be queued asynchronously — a slow response may time out and trigger another attempt.
What stops retries
| Response | Retries? |
|---|---|
| 2xx | No — success |
| Network error / timeout | Yes |
| 408, 429 | Yes |
| 5xx | Yes |
| Other 4xx (401, 404, etc.) | No — fix your endpoint |
Non-retriable failures appear once in the audit log as Delivery failed with no further Retry scheduled entries.
Verifying Webhooks
Use the SDK helper instead of implementing crypto verification yourself:
import { verifyRobotRockWebhook } from "robotrock";
const payload = await verifyRobotRockWebhook(request);verifyRobotRockWebhook():
- validates
x-robotrock-signature - reads
ROBOTROCK_WEBHOOK_SECRET - parses and validates required payload fields
- returns typed payload data including request headers
- throws
RobotRockWebhookErrorwith machine-readable error codes
Webhook signing secret
You must set ROBOTROCK_WEBHOOK_SECRET in your app environment. For example in Next.js:
# .env.local
ROBOTROCK_WEBHOOK_SECRET=rrwhsec_your_shared_secretCreate or rotate this secret in the RobotRock app from your workspace settings where webhook secrets are managed, then update your deployment env vars to match.
Trigger.dev Integration
When you use the Trigger.dev tasks in the SDK, you do not need to configure a client webhook yourself. sendToHumanTask creates a wait token and sets the token URL as the client webhook automatically.
Vercel Workflow Integration
When you use sendToHumanInWorkflow, the SDK creates a workflow webhook and sets webhook.url as the RobotRock handler URL automatically. See Vercel Workflow.
No webhook?
If you omit webhook on createClient, sendToHuman() blocks and polls the API until the task is handled. You cannot set webhook and polling on the same client. See Polling for intervalMs, timeoutMs, and error handling.