This guide walks through deploying a production webhook endpoint that handles AgentMark events. Each adapter provides a webhook handler via its /runner export that processes prompt and dataset events.
For local development and testing, use agentmark dev --tunnel instead of deploying a full endpoint. See Test Webhook for details.
Prerequisites
- Node.js 18+ environment
- A web framework (Next.js, Express, Hono, etc.)
- AgentMark API credentials from the dashboard
1. Get Your Credentials
- Open your app in the AgentMark dashboard
- Navigate to Settings
- Under “Webhook Settings”, find:
- Webhook URL — where you’ll enter your production endpoint URL
- Webhook Secret — used for signature verification
- API Key and App ID — used by the SDK to load prompts and send telemetry
Keep your webhook secret and API credentials secure. Use environment variables — never commit them to source control.
2. Install Dependencies
Install the packages for your adapter:
AI SDK v5 (Vercel)
AI SDK v4 (Vercel)
Claude Agent SDK
Mastra
npm install @agentmark-ai/sdk @agentmark-ai/ai-sdk-v5-adapter @agentmark-ai/shared-utils @agentmark-ai/prompt-core ai @ai-sdk/openai
npm install @agentmark-ai/sdk @agentmark-ai/ai-sdk-v4-adapter @agentmark-ai/shared-utils @agentmark-ai/prompt-core ai @ai-sdk/openai
npm install @agentmark-ai/sdk @agentmark-ai/claude-agent-sdk-adapter @agentmark-ai/shared-utils @agentmark-ai/prompt-core @anthropic-ai/claude-agent-sdk
npm install @agentmark-ai/sdk @agentmark-ai/mastra-v0-adapter @agentmark-ai/shared-utils @agentmark-ai/prompt-core @mastra/core
Set up environment variables:
AGENTMARK_API_KEY=your_api_key
AGENTMARK_APP_ID=your_app_id
AGENTMARK_WEBHOOK_SECRET=your_webhook_secret
Create your AgentMark client. This is typically in a shared agentmark-client.ts file:
AI SDK v5 (Vercel)
AI SDK v4 (Vercel)
Claude Agent SDK
Mastra
import { AgentMarkSDK } from "@agentmark-ai/sdk";
import {
createAgentMarkClient,
VercelAIModelRegistry,
VercelAIToolRegistry,
EvalRegistry,
} from "@agentmark-ai/ai-sdk-v5-adapter";
import { openai } from "@ai-sdk/openai";
export const sdk = new AgentMarkSDK({
apiKey: process.env.AGENTMARK_API_KEY,
appId: process.env.AGENTMARK_APP_ID,
});
sdk.initTracing({ disableBatch: true });
const modelRegistry = new VercelAIModelRegistry();
modelRegistry.registerModels(
["gpt-4o", "gpt-4o-mini"],
(name) => openai(name)
);
const toolRegistry = new VercelAIToolRegistry<any>();
export const evalRegistry = new EvalRegistry();
export const client = createAgentMarkClient({
modelRegistry,
toolRegistry,
evalRegistry,
loader: sdk.getFileLoader(),
});
import { AgentMarkSDK } from "@agentmark-ai/sdk";
import {
createAgentMarkClient,
VercelAIModelRegistry,
VercelAIToolRegistry,
EvalRegistry,
} from "@agentmark-ai/ai-sdk-v4-adapter";
import { openai } from "@ai-sdk/openai";
export const sdk = new AgentMarkSDK({
apiKey: process.env.AGENTMARK_API_KEY,
appId: process.env.AGENTMARK_APP_ID,
});
sdk.initTracing({ disableBatch: true });
const modelRegistry = new VercelAIModelRegistry();
modelRegistry.registerModels(
["gpt-4o", "gpt-4o-mini"],
(name) => openai(name)
);
const toolRegistry = new VercelAIToolRegistry<any>();
export const evalRegistry = new EvalRegistry();
export const client = createAgentMarkClient({
modelRegistry,
toolRegistry,
evalRegistry,
loader: sdk.getFileLoader(),
});
import { AgentMarkSDK } from "@agentmark-ai/sdk";
import {
createAgentMarkClient,
ClaudeAgentModelRegistry,
ClaudeAgentToolRegistry,
EvalRegistry,
} from "@agentmark-ai/claude-agent-sdk-adapter";
export const sdk = new AgentMarkSDK({
apiKey: process.env.AGENTMARK_API_KEY,
appId: process.env.AGENTMARK_APP_ID,
});
sdk.initTracing({ disableBatch: true });
const modelRegistry = new ClaudeAgentModelRegistry();
const toolRegistry = new ClaudeAgentToolRegistry();
export const evalRegistry = new EvalRegistry();
export const client = createAgentMarkClient({
modelRegistry,
toolRegistry,
evalRegistry,
loader: sdk.getFileLoader(),
});
import { AgentMarkSDK } from "@agentmark-ai/sdk";
import {
createAgentMarkClient,
MastraModelRegistry,
MastraToolRegistry,
EvalRegistry,
} from "@agentmark-ai/mastra-v0-adapter";
export const sdk = new AgentMarkSDK({
apiKey: process.env.AGENTMARK_API_KEY,
appId: process.env.AGENTMARK_APP_ID,
});
sdk.initTracing({ disableBatch: true });
const modelRegistry = new MastraModelRegistry();
const toolRegistry = new MastraToolRegistry();
export const evalRegistry = new EvalRegistry();
export const client = createAgentMarkClient({
modelRegistry,
toolRegistry,
evalRegistry,
loader: sdk.getFileLoader(),
});
4. Create the Webhook Endpoint
Create a POST endpoint that verifies signatures and dispatches events to the webhook handler. The example below uses Next.js App Router.
AI SDK v5 (Vercel)
AI SDK v4 (Vercel)
Claude Agent SDK
Mastra
app/api/agentmark/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignature } from "@agentmark-ai/shared-utils";
import { VercelAdapterWebhookHandler } from "@agentmark-ai/ai-sdk-v5-adapter/runner";
import { client } from "@/lib/agentmark-client";
export const dynamic = "force-dynamic";
const handler = new VercelAdapterWebhookHandler(client);
export async function POST(request: NextRequest) {
const payload = await request.json();
const signature = request.headers.get("x-agentmark-signature-256");
// 1. Verify signature
if (
!signature ||
!(await verifySignature(
process.env.AGENTMARK_WEBHOOK_SECRET!,
signature,
JSON.stringify(payload)
))
) {
return NextResponse.json({ message: "Invalid signature" }, { status: 401 });
}
try {
const { event } = payload;
// 2. Handle prompt runs
if (event.type === "prompt-run") {
const response = await handler.runPrompt(event.data.ast, {
shouldStream: true,
customProps: event.data.customProps,
});
if (response.type === "stream") {
return new Response(response.stream, {
headers: { ...response.streamHeader },
});
}
return NextResponse.json(response);
}
// 3. Handle dataset runs
if (event.type === "dataset-run") {
const response = await handler.runExperiment(
event.data.ast,
event.data.experimentId,
event.data.datasetPath
);
return new Response(response.stream, {
headers: { ...response.streamHeaders },
});
}
// 4. Handle alerts
if (event.type === "alert") {
console.log("Alert received:", event.data);
return NextResponse.json({ message: "Alert processed" });
}
return NextResponse.json(
{ message: `Unknown event type: ${event.type}` },
{ status: 400 }
);
} catch (error) {
console.error("Webhook error:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 }
);
}
}
app/api/agentmark/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignature } from "@agentmark-ai/shared-utils";
import { VercelAdapterWebhookHandler } from "@agentmark-ai/ai-sdk-v4-adapter/runner";
import { client } from "@/lib/agentmark-client";
export const dynamic = "force-dynamic";
const handler = new VercelAdapterWebhookHandler(client);
export async function POST(request: NextRequest) {
const payload = await request.json();
const signature = request.headers.get("x-agentmark-signature-256");
if (
!signature ||
!(await verifySignature(
process.env.AGENTMARK_WEBHOOK_SECRET!,
signature,
JSON.stringify(payload)
))
) {
return NextResponse.json({ message: "Invalid signature" }, { status: 401 });
}
try {
const { event } = payload;
if (event.type === "prompt-run") {
const response = await handler.runPrompt(event.data.ast, {
shouldStream: true,
customProps: event.data.customProps,
});
if (response.type === "stream") {
return new Response(response.stream, {
headers: { ...response.streamHeader },
});
}
return NextResponse.json(response);
}
if (event.type === "dataset-run") {
const response = await handler.runExperiment(
event.data.ast,
event.data.experimentId,
event.data.datasetPath
);
return new Response(response.stream, {
headers: { ...response.streamHeaders },
});
}
if (event.type === "alert") {
console.log("Alert received:", event.data);
return NextResponse.json({ message: "Alert processed" });
}
return NextResponse.json(
{ message: `Unknown event type: ${event.type}` },
{ status: 400 }
);
} catch (error) {
console.error("Webhook error:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 }
);
}
}
app/api/agentmark/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignature } from "@agentmark-ai/shared-utils";
import { ClaudeAgentWebhookHandler } from "@agentmark-ai/claude-agent-sdk-adapter/runner";
import { client } from "@/lib/agentmark-client";
export const dynamic = "force-dynamic";
const handler = new ClaudeAgentWebhookHandler(client);
export async function POST(request: NextRequest) {
const payload = await request.json();
const signature = request.headers.get("x-agentmark-signature-256");
if (
!signature ||
!(await verifySignature(
process.env.AGENTMARK_WEBHOOK_SECRET!,
signature,
JSON.stringify(payload)
))
) {
return NextResponse.json({ message: "Invalid signature" }, { status: 401 });
}
try {
const { event } = payload;
if (event.type === "prompt-run") {
const response = await handler.runPrompt(event.data.ast, {
shouldStream: true,
customProps: event.data.customProps,
});
if (response.type === "stream") {
return new Response(response.stream, {
headers: { ...response.streamHeader },
});
}
return NextResponse.json(response);
}
if (event.type === "dataset-run") {
const response = await handler.runExperiment(
event.data.ast,
event.data.experimentId,
event.data.datasetPath
);
return new Response(response.stream, {
headers: { ...response.streamHeaders },
});
}
if (event.type === "alert") {
console.log("Alert received:", event.data);
return NextResponse.json({ message: "Alert processed" });
}
return NextResponse.json(
{ message: `Unknown event type: ${event.type}` },
{ status: 400 }
);
} catch (error) {
console.error("Webhook error:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 }
);
}
}
app/api/agentmark/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignature } from "@agentmark-ai/shared-utils";
import { MastraAdapterWebhookHandler } from "@agentmark-ai/mastra-v0-adapter/runner";
import { client } from "@/lib/agentmark-client";
export const dynamic = "force-dynamic";
const handler = new MastraAdapterWebhookHandler(client);
export async function POST(request: NextRequest) {
const payload = await request.json();
const signature = request.headers.get("x-agentmark-signature-256");
if (
!signature ||
!(await verifySignature(
process.env.AGENTMARK_WEBHOOK_SECRET!,
signature,
JSON.stringify(payload)
))
) {
return NextResponse.json({ message: "Invalid signature" }, { status: 401 });
}
try {
const { event } = payload;
if (event.type === "prompt-run") {
const response = await handler.runPrompt(event.data.ast, {
shouldStream: true,
customProps: event.data.customProps,
});
if (response.type === "stream") {
return new Response(response.stream, {
headers: { ...response.streamHeader },
});
}
return NextResponse.json(response);
}
if (event.type === "dataset-run") {
const response = await handler.runExperiment(
event.data.ast,
event.data.experimentId,
event.data.datasetPath
);
return new Response(response.stream, {
headers: { ...response.streamHeaders },
});
}
if (event.type === "alert") {
console.log("Alert received:", event.data);
return NextResponse.json({ message: "Alert processed" });
}
return NextResponse.json(
{ message: `Unknown event type: ${event.type}` },
{ status: 400 }
);
} catch (error) {
console.error("Webhook error:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 }
);
}
}
Deploy your endpoint
Deploy your application to a publicly accessible URL (e.g., Vercel, Railway, AWS).
Add the webhook URL
In the AgentMark dashboard, go to Settings and enter your endpoint URL (e.g., https://your-app.vercel.app/api/agentmark).
Set the webhook secret
Paste the webhook secret from the dashboard into your deployment’s environment variables as AGENTMARK_WEBHOOK_SECRET.
Test
Open a prompt in the platform editor and click Run. The result should appear in the platform.
Security Best Practices
- Always verify signatures — reject requests with missing or invalid
x-agentmark-signature-256 headers
- Use HTTPS — your production endpoint must use HTTPS
- Store secrets in environment variables — never hardcode credentials
- Return proper status codes —
401 for auth failures, 400 for bad requests, 500 for server errors
Next Steps
Have Questions?
We’re here to help! Choose the best way to reach us: