Pradra StudioDocs

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 toolsNew 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 302 to 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