> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentmark.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive alert notifications from AgentMark Cloud via webhooks

<Info>**Paid feature.** Webhook alert delivery requires a Growth, Team, or Enterprise plan. The `alerts_enabled` entitlement is off on the Free plan.</Info>

AgentMark webhooks deliver alert notifications to your application when cost, latency, error-rate, or evaluation-score alerts trigger or resolve.

## 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.

## Setup

### 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.

1. 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.
2. Navigate to **Settings → Integrations**.
3. The **Webhook Url** form has two fields:
   * **Webhook Url**: your webhook endpoint URL.
   * **Secret Key**: the secret used for signature verification.

<Warning>
  Keep your webhook secret secure. Use environment variables and never commit it to source control.
</Warning>

### 2. Install dependencies

```bash theme={null}
npm install @agentmark-ai/shared-utils
```

### 3. Create the webhook endpoint

Set up environment variables:

```env theme={null}
AGENTMARK_WEBHOOK_SECRET=your_webhook_secret
```

Create a POST endpoint that verifies signatures and handles alert events. This example uses Next.js App Router:

```typescript app/api/agentmark-alerts/route.ts theme={null}
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

<Steps>
  <Step title="Deploy your endpoint">
    Deploy your application to a publicly accessible URL (for example, Vercel, Railway, AWS).
  </Step>

  <Step title="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 (for example, `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.
  </Step>

  <Step title="Set the webhook secret">
    Add the **Secret Key** from the same **Webhook Url** form to your deployment's environment variables as `AGENTMARK_WEBHOOK_SECRET`.
  </Step>

  <Step title="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 won't POST to your webhook URL even if you configured one.
  </Step>

  <Step title="Test">
    Configure an alert in the Dashboard (for example, cost threshold), trigger the condition, and verify your endpoint receives the event.
  </Step>
</Steps>

## Event format

```json theme={null}
{
  "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()`.
* AgentMark includes `commitSha` inside `alert` when the app has a commit SHA on record at trigger time; otherwise it omits the field. On `resolved` events it always omits the field.
* `environmentScope` is **always present**: the name of the environment the alert targets, or the literal string `all-envs` for an app-wide alert.
* AgentMark includes `environmentName` only when the alert targets an environment, and the field carries that environment's name.
* AgentMark includes the three `evaluation*` fields on `alert` 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

```typescript theme={null}
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](#event-format):

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

1. **Always verify signatures.** Reject requests with missing or invalid `x-agentmark-signature-256` headers.
2. **Use HTTPS.** Your production endpoint must use HTTPS.
3. **Store secrets in environment variables.** Never hardcode credentials.
4. **Return proper status codes.** `401` for auth failures, `400` for bad requests, `500` for server errors.
5. **Respond quickly.** Return a `200` status promptly, then process the alert asynchronously if needed.
6. **Route by type.** Send cost alerts to finance channels, latency alerts to engineering, and so on.

<div className="mt-8 rounded-lg bg-blue-50 p-6 dark:bg-blue-900/30">
  <h3 className="font-semibold mb-3">Have questions?</h3>
  <p className="mb-4">Reach out any time:</p>

  <ul>
    <li>
      Email the team at <a href="mailto:hello@agentmark.co" className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">[hello@agentmark.co](mailto:hello@agentmark.co)</a> for support
    </li>

    <li>
      Schedule an <a href="https://cal.com/ryan-randall/enterprise" className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200">Enterprise Demo</a> to learn about AgentMark's business solutions
    </li>
  </ul>
</div>
