Webhook tools
The same tool definitions as a worker, reached the other way: Pradra calls an HTTPS endpoint you expose. Your handler code does not change — only how the call arrives.
Pick a worker unless you have a reason not to. It needs no public reachability, no certificate, no signature verification, and works from a laptop or a private VPC. Reach for a webhook when a long-lived process has nowhere to live — a serverless platform, an edge runtime — or when you already run a public API and prefer not to hold an outbound connection open.
Step 1 — Store the signing secret
Every request is signed, and an unsigned webhook cannot be configured at all: it would be an open door onto your business code for anyone who learns the URL.
Generate a strong random value, keep it in your application's environment, and store the same value in Pradra: Secrets → new secret of type opaque. You can also create it inline while creating the tool.
openssl rand -hex 32
Step 2 — Expose the endpoint
Express and other app.post-shaped frameworks:
import express from "express";
import { mountTools } from "@pradra/sdk";
const app = express();
// NOTE: no express.json() on this route — see below.
mountTools(app, {
tools: [getOrderStatus, refundOrder],
secret: process.env.PRADRA_TOOL_SECRET!,
path: "/pradra/tools", // default
});
Anything with a standard Request/Response — Hono, Next.js route handlers,
Cloudflare Workers, Deno, Bun:
import { createWebhookHandler } from "@pradra/sdk";
export const POST = createWebhookHandler({
tools: [getOrderStatus, refundOrder],
secret: process.env.PRADRA_TOOL_SECRET!,
});
Or drive verification yourself from a raw body and headers with
createToolHandler(opts), which returns (rawBody, headers) => { status, body }.
Do not parse the body before verifying.
express.json()on this route re-serializes the payload, which changes the bytes the signature covers — the single most common cause of a webhook that "randomly" rejects valid calls. The SDK reads the raw body itself.
The SDK uses Web Crypto rather than node:crypto, so it runs anywhere fetch
does, and compares signatures in constant time.
Step 3 — Register the tool against that URL
In the Studio: Agent tools → New tool → backing External → transport Webhook. Fill in the URL, pick the signing secret, and optionally a timeout.
The platform will refuse:
- Anything that is not
https. - A private, loopback, link-local or metadata address — checked when you save and again against the address the hostname actually resolves to at call time. Save-time checking alone loses to a hostname re-pointed at an internal address a minute later, which would turn tool registration into a way to read Pradra's own network.
- A redirect. A
302to an internal address would bypass every check above it.
If your URL is rejected, the reason appears on the field. It is a real refusal, not a validation quirk.
Step 4 — Verify it end to end
Grant the tool to an agent and run it, exactly as with a worker. The trace on the run shows the call and its observation.
Checkpoint: your endpoint logged a request, the signature verified, and the run's trace shows the result.
What arrives, and what you must check
Each request carries the tool name, the arguments (already validated against
your declared contract before dispatch), the principal, and a call id.
The SDK handler verifies three things, and all three matter:
The signature covers timestamp . call_id . body — not the body alone.
Signing only the body would make every request replayable forever under a fresh
timestamp header.
The clock, within five minutes by default. This is what bounds the replay window to something a cache can cover.
The call id, against a recent-id cache. A repeat answers 409, not a cached success: the handler does not hold the previous result, and returning "ok" for a call it never ran is a lie the platform would record as an observation.
Retries reuse the same call id, so use it as an idempotency key for your own writes.
Retries
Only transport failures, 5xx, 429 and 408 are retried, and only for
read_only tools. A 4xx from your endpoint means you said no — for a write
tool a re-send could mean a second side effect, so it is not attempted.
A consequential webhook tool still parks for human approval before a single
byte leaves the platform. Approval is not something your endpoint can be
asked to skip.
Switching transports later
Because the tool definition is identical, moving between worker and webhook is a configuration change on the tool — no code edit, no re-testing your handlers. That is the point of keeping them the same shape.
Next
- The worker path, and the concepts behind both: Serving your own tools.
- Everything in one page: API surface.