Paid feature. Webhook alert delivery requires a Growth, Team, or Enterprise plan. The alerts_enabled entitlement is off on the Free plan.
AgentMark webhooks deliver alert notifications to your application when cost, latency, error-rate, or evaluation-score thresholds are breached or resolved.
How it works
When an alert fires or resolves, AgentMark Cloud sends an HTTP POST request to your configured webhook URL. The request body is the event payload and the x-agentmark-signature-256 header is an HMAC-SHA256 signature of the request body (using your webhook secret), formatted as sha256=<hex-digest>. Your endpoint verifies the signature, processes the alert, and returns a 2xx status.
1. Get your webhook secret
AgentMark stores one webhook URL and one secret per environment, and alerts reuse that configuration. An alert scoped to an environment delivers through that environment’s webhook; an app-wide alert delivers through the default (dev) environment’s webhook.
- Open your app in the AgentMark Dashboard and select the environment whose webhook you want to configure in the breadcrumb. Switching environment shows that environment’s values.
- Navigate to Settings → Integrations.
- The Webhook Url form has two fields:
- Webhook Url: your webhook endpoint URL.
- Secret Key: the secret used for signature verification.
Keep your webhook secret secure. Use environment variables and never commit it to source control.
2. Install dependencies
npm install @agentmark-ai/shared-utils
3. Create the webhook endpoint
Set up environment variables:
AGENTMARK_WEBHOOK_SECRET=your_webhook_secret
Create a POST endpoint that verifies signatures and handles alert events. This example uses Next.js App Router:
app/api/agentmark-alerts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySignature } from "@agentmark-ai/shared-utils";
export const dynamic = "force-dynamic";
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 alert events
if (event.type === "alert") {
const { alert, message, timestamp } = event.data;
console.log(
`Alert ${alert.status}: ${alert.type} — ${message} (${timestamp})`
);
// Route to your notification system (Slack, PagerDuty, email, etc.)
// await sendSlackNotification(alert, message);
return NextResponse.json({
message: "Alert processed",
alertId: alert.id,
status: alert.status,
});
}
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 }
);
}
}
4. Deploy and configure
Deploy your endpoint
Deploy your application to a publicly accessible URL (e.g., Vercel, Railway, AWS).
Add the webhook URL
In the AgentMark Dashboard, select the environment in the breadcrumb, go to Settings → Integrations, and enter your endpoint URL in the Webhook Url form (e.g., https://your-app.vercel.app/api/agentmark-alerts). Repeat for each environment whose alerts should deliver: an environment-scoped alert uses its own environment’s webhook, and app-wide alerts use the default (dev) environment’s webhook.
Set the webhook secret
Add the Secret Key from the same Webhook Url form to your deployment’s environment variables as AGENTMARK_WEBHOOK_SECRET.
Enable webhook delivery on the alert
Webhook delivery is opt-in per alert. When you create or edit an alert in the Dashboard (under Alerts), toggle on Custom Webhook; it’s off by default. Without this toggle, AgentMark will not POST to your webhook URL even if one is configured.
Test
Configure an alert in the Dashboard (e.g., cost threshold), trigger the condition, and verify your endpoint receives the event.
Event format
{
"event": {
"type": "alert",
"data": {
"alert": {
"id": "string",
"currentValue": 0,
"threshold": 0,
"status": "triggered | resolved",
"timeWindow": 60,
"type": "cost | latency | error_rate | evaluation_score",
"commitSha": "string (optional)",
"environmentScope": "string (env name, or all-envs for app-wide alerts)",
"environmentName": "string (only when the alert is scoped to an environment)",
"evaluationName": "string (only when type is evaluation_score)",
"evaluationAggregation": "avg | individual (only when type is evaluation_score)",
"evaluationThresholdDirection": "above | below (only when type is evaluation_score)"
},
"message": "string",
"timestamp": 1712764245000
}
}
}
timeWindow is the measurement window in minutes (number).
timestamp is a Unix epoch in milliseconds (number), from Date.now().
commitSha is included inside alert when the app has a commit SHA on record at trigger time; otherwise it’s omitted. On resolved events it is always omitted.
environmentScope is always present: the name of the environment the alert is scoped to, or the literal string all-envs for an app-wide alert.
environmentName is included only when the alert is scoped to an environment, and carries that environment’s name.
- The three
evaluation* fields on alert are included only when type is evaluation_score.
- The
message string ends with an environment suffix: [env: <name>] for environment-scoped alerts, or [across all envs] for app-wide alerts.
Alert types
| Type | Monitors | Example threshold |
|---|
cost | API usage costs | Spending > $50/day |
latency | Response times | P95 latency > 5000ms |
error_rate | Error frequency | Error rate > 5% |
evaluation_score | Score pipeline | Score < 0.8 average |
Processing alerts
if (event.type === "alert") {
const { alert, message, timestamp } = event.data;
if (alert.status === "triggered") {
switch (alert.type) {
case "cost":
console.log(`Cost alert: ${alert.currentValue} exceeded threshold ${alert.threshold}`);
break;
case "latency":
console.log(`Latency alert: ${alert.currentValue}ms exceeded threshold ${alert.threshold}ms`);
break;
case "error_rate":
console.log(`Error rate alert: ${alert.currentValue}% exceeded threshold ${alert.threshold}%`);
break;
case "evaluation_score":
console.log(`Score alert: ${alert.currentValue} crossed threshold ${alert.threshold}`);
break;
}
} else if (alert.status === "resolved") {
console.log(`Alert ${alert.id} resolved at ${new Date(timestamp).toISOString()}`);
}
}
Integration examples
The examples below share this Alert type, matching the payload described in Event format:
interface Alert {
id: string;
currentValue: number;
threshold: number;
status: "triggered" | "resolved";
timeWindow: number;
type: "cost" | "latency" | "error_rate" | "evaluation_score";
commitSha?: string;
environmentScope: string; // env name, or "all-envs" for app-wide alerts
environmentName?: string; // present only when the alert is env-scoped
evaluationName?: string;
evaluationAggregation?: "avg" | "individual";
evaluationThresholdDirection?: "above" | "below";
}
Slack notifications
async function sendSlackNotification(alert: Alert, message: string) {
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
if (!slackWebhookUrl) return;
await fetch(slackWebhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `AgentMark Alert: ${alert.type.toUpperCase()}`,
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: message },
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Status:*\n${alert.status}` },
{ type: "mrkdwn", text: `*Current:*\n${alert.currentValue}` },
{ type: "mrkdwn", text: `*Threshold:*\n${alert.threshold}` },
{ type: "mrkdwn", text: `*Window:*\n${alert.timeWindow} min` },
],
},
],
}),
});
}
Email notifications
async function sendEmailAlert(alert: Alert, message: string) {
// Use your preferred email service (Resend, SendGrid, AWS SES, etc.)
const emailConfig = {
from: process.env.ALERT_EMAIL_FROM,
to: process.env.ALERT_EMAIL_TO,
subject: `AgentMark Alert: ${alert.type} - ${alert.status}`,
html: `
<h2>AgentMark Alert</h2>
<p>${message}</p>
<table>
<tr><td><strong>Alert ID:</strong></td><td>${alert.id}</td></tr>
<tr><td><strong>Type:</strong></td><td>${alert.type}</td></tr>
<tr><td><strong>Status:</strong></td><td>${alert.status}</td></tr>
<tr><td><strong>Current Value:</strong></td><td>${alert.currentValue}</td></tr>
<tr><td><strong>Threshold:</strong></td><td>${alert.threshold}</td></tr>
</table>
`,
};
// await sendEmail(emailConfig);
}
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.
- Respond quickly. Return a
200 status promptly, then process the alert asynchronously if needed.
- Route by type. Send cost alerts to finance channels, latency alerts to engineering, and so on.
Have questions?
Reach out any time: