# beeps documentation beeps routes alerts from monitoring systems to on-call engineers and AI agents. --- # What is beeps? > Source: https://beeps.dev/docs/ beeps routes alerts from your monitoring systems to on-call engineers and AI agents. When an incident occurs, beeps can notify humans and kick off automated triage at the same time — or sequentially, however you want to set it up. ## The Flow: How Alerts Move Through the System Here's what happens when an incident occurs: ``` Monitoring System (Sentry, Datadog, etc.) ↓ Sends webhook to beeps ↓ Alert Created ↓ Relay (routing pipeline) ↓ Relay Rules (ordered actions) ├─ [Group: agents] → AI Agent starts working │ (uses Integration credentials) │ └─ [Group: humans] → Find on-call user (from Schedule) → Send notification (via Contact Methods) ``` ## Core concepts ### Relays Relays are alert routing pipelines. When an alert arrives, the relay runs it through an ordered set of rules. Think of a relay as "what should happen when this alert fires." You'll typically have one per service or environment ("Production Alerts", "Staging Alerts"). Set `externalKey` on relays so your config scripts are idempotent. ### Relay rules Rules are the individual actions inside a relay. Three types: - `schedule_notify` — notify the on-call user from a schedule - `webhook` — POST to an external URL (Slack, PagerDuty, etc.) - `agent` — invoke an AI agent (Devin, Cursor) Rules have a `group` and an `order`. Same group = sequential (order 1 runs, then 2, then 3). Different groups = parallel. This is how you control whether agents and humans get notified at the same time or one after the other: ```typescript // Parallel — different groups, both fire immediately Rule 1: { group: "agents", order: 1, ruleType: "agent" } Rule 2: { group: "humans", order: 1, ruleType: "schedule_notify" } // Sequential — same group, agent tries first Rule 1: { group: "default", order: 1, ruleType: "agent" } Rule 2: { group: "default", order: 2, ruleType: "schedule_notify" } ``` ### Schedules Schedules define on-call rotations — who is on-call and when it rotates. A schedule becomes active at `startAt`, then hands off on a daily or weekly cadence using `handoffDay` and `handoffTime` (UTC). If you omit `startAt`, beeps starts the schedule immediately. Members rotate in the order they were added. ### Contact methods Each user needs at least one contact method (email or SMS) to receive notifications. We recommend both. Phone numbers should be in E.164 format (`+14155551234`). ### Integrations Integrations store API credentials for AI agents (Devin, Cursor). The keys are encrypted and never returned in API responses. Don't confuse these with observability integrations — agent integrations tell beeps how to talk to agents, while observability integrations are webhooks that monitoring tools use to send alerts into beeps. ### Webhooks Each relay gets a webhook URL (`https://hooks.beeps.dev/{webhookKey}`) that monitoring systems POST to. When a request comes in, beeps detects the provider, normalizes the payload, creates an alert, and kicks off the relay. ### Alerts Alerts are incidents created from webhook payloads — things like "High CPU usage on web-01" with a severity of `critical`. They carry a `title`, `severity` (critical/high/medium/low/info), optional `message`, and arbitrary `metadata` JSON for context like server names or error rates. ## Alert Lifecycle 1. **Created** - Alert arrives via webhook 2. **Responded** - Someone (human or AI) is working on it 3. **Assigned** - Explicitly assigned to a specific user 4. **Resolved** - Issue is fixed Responding to an alert cancels any pending relay executions, preventing duplicate notifications. ## Typical Setup Order When configuring beeps for the first time, follow this order: 1. **Create a Relay** - The routing pipeline 2. **Create a Schedule** - Define on-call rotation 3. **Add Schedule Members** - Add team members to rotation 4. **Add Contact Methods** - Configure email/SMS for users 5. **Create Integration** (optional) - Set up AI agent credentials 6. **Create Relay Rules** - Define what happens when alerts arrive 7. **Test** - Send a test alert to verify everything works **Ready to get started?** Follow the [Quickstart](/docs/quickstart) to set up relays, schedules, notifications, and AI agents. ## Next Steps - [Quickstart](/docs/quickstart) - Set up humans and AI agents - [Best Practices](/docs/best-practices/) - Configuration guidelines - [Relays](/docs/relays/overview/) - Alert routing in depth - [Schedules](/docs/schedules/overview/) - On-call rotations ## Questions? **Q: What's the difference between Agent Integrations and Observability Integrations?** A: Agent Integrations store credentials for AI agents (Devin, Cursor Cloud Agent) so beeps can invoke them. Observability Integrations are webhooks that monitoring systems (Sentry, Datadog) use to send alerts into beeps. **Q: Can I have multiple relays?** A: Yes. You might have separate relays for production vs staging, or different relays for different services. **Q: What happens if no one is on-call?** A: The `schedule_notify` rule won't send notifications. Make sure your schedule always has at least one member. **Q: Can I test without sending real alerts?** A: Yes — just POST a test payload to your webhook URL, or use the SDK to create test alerts programmatically. --- # Quickstart - Humans + AI Agents > Source: https://beeps.dev/docs/quickstart/ import { Tabs, TabItem } from "@astrojs/starlight/components"; This guide walks you through setting up a complete beeps system with human responders and AI agents working in parallel. New to beeps? Read [Core Concepts](/docs/) first to understand how the pieces fit together. ## What You'll Build By the end of this guide, you'll have: - A relay that receives alerts - A schedule with team members rotating on-call - Contact methods (email and SMS) for notifications - An AI agent integration for automated triage - Parallel routing: alerts go to both humans and AI simultaneously ## Prerequisites - A [beeps](https://beeps.dev) account - An API key from your organization settings (looks like `bk_xxxxxxxx`) :::tip[Prefer the CLI or MCP?] Skip to [Quickstart via CLI](#quickstart-via-cli) to set up from the command line, or [Quickstart via MCP](#quickstart-via-mcp) to manage alerts from Claude Code or Codex. ::: ## Step 1: Install the SDK and Initialize the Client ```bash npm install @beepsdev/sdk ``` ```bash pnpm add @beepsdev/sdk ``` ```bash bun add @beepsdev/sdk ``` Set your API key: ```bash export BEEPS_API_KEY="bk_your_api_key_here" ``` Create a new file `beeps.config.ts`: ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); ``` ## Step 2: Create a Relay Create a relay, which is the pipeline that receives alerts and routes them through rules. ```typescript const relay = await client.relay.create({ name: "production relay", description: "routes critical production incidents", externalKey: "production::relay", }); ``` :::tip[Use externalKey for Idempotency] Always set `externalKey` on relays, schedules, and rules. This makes your config **idempotent** — safe to re-run without creating duplicates. Think of `externalKey` as a stable identifier you control, while `id` is generated by beeps. ::: **Related:** [Managing Relays](/docs/relays/manage/) ## Step 3: Create an On-Call Schedule Create a schedule to define who is on-call and when rotations happen. ```typescript const schedule = await client.schedule.create({ name: "Primary On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", // UTC externalKey: "primary::schedule", }); ``` Omitting `startAt` here is intentional. beeps defaults it to the current time so the schedule starts immediately and you can test alerts right away. **Related:** [Managing Schedules](/docs/schedules/manage/) ## Step 4: Add Team Members to Schedule Add users to the on-call rotation by email address: ```typescript await client.schedule.addMember(schedule.id, { email: "alice@example.com", }); await client.schedule.addMember(schedule.id, { email: "bob@example.com", }); ``` You can also add members by user ID if you have it: ```typescript await client.schedule.addMember(schedule.id, { userId: "usr_alice", }); ``` Members rotate in the order they're added. **Related:** [Schedule Members](/docs/schedules/manage/#adding-schedule-members) ## Step 5: Configure Contact Methods Contact methods define how users receive notifications. Your signup email is automatically added as a verified contact method when you create your account. To add SMS or additional email contact methods, go to **Settings > Profile** in the beeps web UI. SMS contact methods require verification via a confirmation text. **Related:** [Contact Methods Overview](/docs/contact-methods/overview/) :::tip[You can stop here if you don't need AI agents] At this point, you have a working human-only on-call setup. Skip to [Step 7](#step-7-create-parallel-relay-rules) and create only the human notification rule (Rule 2). ::: ## Step 6: Set Up AI Agent Integration Add an integration to store credentials for your AI agent. For this quickstart, use Cursor and get an API key from the [Cursor dashboard](https://cursor.com/dashboard?tab=cloud-agents). :::tip[Cursor requirements] Enable Privacy Mode in your Cursor account settings before using the API. Cursor agent rules also require a `repository` URL in the rule config. ::: ```typescript const integration = await client.integration.create({ name: "Cursor Agent", provider: "cursor", apiKey: process.env.CURSOR_API_KEY, metadata: { environment: "production", }, }); ``` **Related:** [Managing Integrations](/docs/agent-integrations/manage/) ## Step 7: Create Parallel Relay Rules Create rules that route alerts to responders. These rules run in parallel so AI agents and humans both get notified simultaneously. ### Rule 1: AI Agent (Cursor) ```typescript await client.relay.rules.create(relay.id, { name: "AI Agent Auto-Triage", externalKey: "agents::cursor", ruleType: "agent", group: "agents", order: 1, config: { agentType: "cursor", integrationId: integration.id, repository: "https://github.com/your-org/your-repo", autoCreatePr: true, pollInterval: 30000, maxPollAttempts: 120, }, enabled: true, }); ``` ### Rule 2: Human On-Call ```typescript await client.relay.rules.create(relay.id, { name: "Notify On-Call Engineer", externalKey: "humans::primary::schedule-notify", ruleType: "schedule_notify", group: "humans", order: 1, config: { scheduleId: schedule.id, }, enabled: true, }); ``` Rules in different groups (`"agents"` vs `"humans"`) run in parallel. Both the AI agent and human receive the alert simultaneously. **Related:** [Relay Rules](/docs/relays/rules/) ## Step 8: Verify Your Setup List all rules to confirm configuration: ```typescript const rules = await client.relay.rules.list(relay.id); console.log("\nRelay Rules:"); rules.forEach((rule) => { console.log(` [${rule.group}] ${rule.name}`); console.log(` Type: ${rule.ruleType}`); console.log(` Enabled: ${rule.enabled}`); }); ``` ## Complete Config Here's the complete `beeps.config.ts` combining all steps: ```typescript import { BeepsClient } from "@beepsdev/sdk"; async function configure() { const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); // 1. Create relay const relay = await client.relay.create({ name: "production relay", description: "routes critical production incidents", externalKey: "production::relay", }); // 2. Create schedule const schedule = await client.schedule.create({ name: "Primary On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", // startAt omitted -> schedule starts immediately externalKey: "primary::schedule", }); // 3. Add team members by email await client.schedule.addMember(schedule.id, { email: "alice@example.com" }); await client.schedule.addMember(schedule.id, { email: "bob@example.com" }); // 4. Contact methods are managed in Settings > Profile in the web UI. // Each user's signup email is automatically added as a verified contact method. // SMS and additional emails can be added from the profile settings. // 5. Create AI integration const integration = await client.integration.create({ name: "Cursor Production Agent", provider: "cursor", apiKey: process.env.CURSOR_API_KEY, externalKey: "agents::cursor::integration", }); // 6. Create relay rules (parallel: agents + humans) await client.relay.rules.create(relay.id, { name: "AI Agent Auto-Triage", externalKey: "agents::cursor", ruleType: "agent", group: "agents", order: 1, config: { agentType: "cursor", integrationId: integration.id, repository: "https://github.com/your-org/your-repo", autoCreatePr: true, }, enabled: true, }); await client.relay.rules.create(relay.id, { name: "Notify On-Call Engineer", externalKey: "humans::primary::schedule-notify", ruleType: "schedule_notify", group: "humans", order: 1, config: { scheduleId: schedule.id, }, enabled: true, }); // 7. Verify const rules = await client.relay.rules.list(relay.id); console.log("\nRelay Rules:"); rules.forEach((rule) => { console.log(` [${rule.group}] ${rule.name}`); console.log(` Type: ${rule.ruleType}`); console.log(` Enabled: ${rule.enabled}`); }); } configure().catch(console.error); ``` Run it: ```bash export BEEPS_API_KEY="bk_your_key_here" export CURSOR_API_KEY="cursor_api_key_here" npx tsx beeps.config.ts ``` ## Test Your Configuration Send a test alert to your relay's webhook URL (found in your relay's details): ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_ID \ -H "Content-Type: application/json" \ -d '{ "title": "Test Alert", "message": "Testing the on-call system", "severity": "high" }' ``` You should see: - The AI agent (Cursor) receives the alert and starts triaging - The on-call engineer receives an email/SMS notification - Both happen simultaneously! ## Deploy Your Config Since `beeps.config.ts` uses `externalKey` everywhere, it's idempotent and safe to re-run. Treat it like database migrations — check it into your repo and run it in CI on merge. ### GitHub Actions ```yaml name: Deploy beeps config on: push: branches: [main] paths: [beeps.config.ts] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm install @beepsdev/sdk - run: npx tsx beeps.config.ts env: BEEPS_API_KEY: ${{ secrets.BEEPS_API_KEY }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} ``` Changes to your on-call config go through PR review like any other code change. ## Understanding Parallel Execution ``` Alert Received | Relay: "production relay" | |-- [agents] AI Agent Auto-Triage --> Cursor starts working | |-- [humans] Notify On-Call Engineer --> Email/SMS sent ``` Both rules run in parallel because they're in different groups (`"agents"` and `"humans"`). ## Common Customizations ### Sequential Instead of Parallel To run AI first, then escalate to humans, use the same group: ```typescript await client.relay.rules.create(relay.id, { name: "Try AI first", ruleType: "agent", group: "default", order: 1, config: { agentType: "cursor", integrationId: integration.id, repository: "https://github.com/your-org/your-repo", }, }); await client.relay.rules.create(relay.id, { name: "Escalate to humans", ruleType: "schedule_notify", group: "default", order: 2, config: { scheduleId: schedule.id }, }); ``` ### Add a Secondary Schedule ```typescript await client.relay.rules.create(relay.id, { name: "Escalate to backup", ruleType: "schedule_notify", group: "humans", order: 2, config: { scheduleId: backupSchedule.id }, }); ``` ## Next Steps - [Add more team members](/docs/schedules/manage/#adding-schedule-members) to the rotation - [Configure escalation rules](/docs/relays/rules/) for unresponded alerts - [Set up webhooks](/docs/relays/rules/#webhook) for Slack/PagerDuty integration - [Monitor active alerts](/docs/alerts/manage/) through the SDK or CLI - [Connect monitoring tools](/docs/observability-integrations/overview/) like Sentry or Datadog - [Set up MCP tools](/docs/using-beeps/mcp-server/) for Claude Code or Codex - [Manage everything from the CLI](/docs/cli/config/) with config-as-code ## Quickstart via CLI You can set up the same system entirely from the command line. ### Step 1: Install and authenticate ```bash npm install -g @beepsdev/cli export BEEPS_API_KEY="bk_your_key_here" ``` ### Step 2: Create a relay ```bash beeps relay create --name "production relay" \ --external-key production::relay \ --description "routes critical production incidents" ``` ### Step 3: Create a schedule ```bash beeps schedule create \ --name "Primary On-Call" \ --relay-id rly_abc123 \ --type weekly \ --handoff-day monday \ --handoff-time 09:00 \ --external-key primary::schedule ``` ### Step 4: Add team members ```bash beeps schedule add-member --schedule-id sch_abc123 --email alice@example.com beeps schedule add-member --schedule-id sch_abc123 --email bob@example.com ``` Members rotate in the order they're added. ### Step 5: Set up an AI agent integration ```bash export CURSOR_API_KEY="cursor_api_key_here" beeps integration create --provider cursor --name "Cursor Agent" --api-key-env CURSOR_API_KEY ``` ### Step 6: Create relay rules Create rules that route alerts to both AI and humans in parallel: ```bash beeps relay rule create --relay-id rly_abc123 \ --name "AI Agent Auto-Triage" \ --rule-type agent \ --group agents \ --order 1 \ --external-key agents::cursor \ --config '{"agentType":"cursor","integrationId":"int_abc123","repository":"https://github.com/your-org/your-repo","autoCreatePr":true}' beeps relay rule create --relay-id rly_abc123 \ --name "Notify On-Call Engineer" \ --rule-type schedule_notify \ --group humans \ --order 1 \ --external-key humans::primary \ --config '{"scheduleId":"sch_abc123"}' ``` Rules in different groups (`agents` vs `humans`) run in parallel. ### Step 7: Verify your setup ```bash beeps relay list beeps relay rule list --relay-id rly_abc123 beeps schedule on-call --schedule-id sch_abc123 beeps integration list ``` ### Respond to alerts Once alerts start flowing: ```bash beeps alert list --active beeps alert on-it --alert-id alr_abc123 beeps alert resolve --alert-id alr_abc123 ``` :::tip[Config-as-Code] Once you're comfortable, you can manage everything declaratively with a config file and `beeps relay apply`. See [CLI Config-as-Code](/docs/cli/config/) for details and CI deployment examples. ::: ## Quickstart via MCP If you use Claude Code, Codex, or any MCP-compatible client, you can manage alerts and on-call directly from your editor. ### Step 1: Connect the MCP server ```bash claude mcp add --transport http --scope user beeps https://mcp.beeps.dev/mcp ``` ```bash codex mcp add beeps --url https://mcp.beeps.dev/mcp codex mcp login beeps --scopes "openid,profile,email,offline_access,beeps.tools.read,beeps.tools.write" ``` ### Step 2: Check who's on call ```text Who's on call for the primary schedule? ``` ### Step 3: Triage alerts ```text Show me all active alerts ``` ```text Are there any critical alerts right now? ``` ### Step 4: Respond to an alert ```text I'm on it for alert alr_abc123 ``` ```text Load the fix context for alert alr_abc123 and help me resolve it ``` ### Step 5: Resolve ```text Mark my response as done with PR https://github.com/org/repo/pull/42 ``` ```text Resolve alert alr_abc123 ``` ### Step 6: Manage schedules ```text Set a schedule override for the primary schedule — put Bob on call from now until Friday ``` For the full list of 27 MCP tools, see [MCP Server](/docs/using-beeps/mcp-server/). ## Troubleshooting If you encounter issues, see the [Troubleshooting Guide](/docs/troubleshooting/) for solutions: - [Alerts not appearing](/docs/troubleshooting/#alerts-are-not-appearing) - [No one receiving notifications](/docs/troubleshooting/#alerts-appear-but-no-one-is-notified) - [AI agent not responding](/docs/troubleshooting/#ai-agent-never-responds) - [Rules not executing](/docs/troubleshooting/#relay-rules-not-firing) --- # Relays Overview > Source: https://beeps.dev/docs/relays/overview/ Relays are alert routing pipelines. They receive alerts via webhooks and process them through ordered rules to notify people, call external APIs, or invoke AI agents. ## Relay Rules Each relay has rules that execute in order. Rule types: - **schedule_notify** - Notify the on-call user from a schedule - **webhook** - Call an external URL (Slack, PagerDuty, etc.) - **agent** - Invoke an AI agent (Devin, Cursor Cloud Agent) Rules are organized into groups. Rules in the same group run sequentially; rules in different groups run in parallel. See [Best Practices](/docs/best-practices/#parallel-vs-sequential-execution) for examples. ## Common Use Cases ### Direct Schedule Notification Route all alerts directly to your on-call schedule: ```typescript const relay = await client.relay.create({ name: "Production Alerts", description: "Critical production incidents", externalKey: "production-relay", // Idempotent - safe to re-run }); await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", externalKey: "notify-oncall", // Idempotent rule creation ruleType: "schedule_notify", config: { scheduleId: "sch_xyz123", }, }); ``` ### Escalation with AI Agent Try an AI agent first, then escalate to humans: ```typescript await client.relay.rules.create(relay.id, { name: "Try AI agent first", externalKey: "agent-first", // Idempotent order: 1, ruleType: "agent", config: { agentType: "devin", integrationId: "int_abc123", }, }); await client.relay.rules.create(relay.id, { name: "Escalate to on-call", externalKey: "escalate-oncall", // Idempotent order: 2, ruleType: "schedule_notify", config: { scheduleId: "sch_xyz123", }, }); ``` ### Webhook Integration Send alerts to external systems: ```typescript await client.relay.rules.create(relay.id, { name: "Post to Slack", externalKey: "slack-webhook", // Idempotent ruleType: "webhook", config: { endpoint: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", method: "POST", headers: { "Content-Type": "application/json", }, payload: { text: "New alert received", }, }, }); ``` --- # Managing Relays > Source: https://beeps.dev/docs/relays/manage/ import { Tabs, TabItem, Code } from '@astrojs/starlight/components'; Create and manage relays using the SDK, API, and CLI. ## Creating a Relay ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); const relay = await client.relay.create({ name: "Production Alerts", description: "Routes critical production incidents", externalKey: "prod-alerts", // Optional: use for easier API access }); console.log(`Created relay: ${relay.id}`); ``` ### Safe Method (No Exceptions) ```typescript const result = await client.relay.createSafe({ name: "Production Alerts", description: "Routes critical production incidents", }); if (result.error) { console.error("Failed to create relay:", result.error.message); } else { console.log(`Created relay: ${result.data.id}`); } ``` ```bash curl -X POST https://api.beeps.dev/v0/relay \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts" }' ``` **Response:** ```json { "relay": { "id": "rly_abc123", "organizationId": "org_xyz789", "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } } ``` ```bash beeps relay create --name "Production Alerts" --external-key prod-alerts --description "Routes critical production incidents" ``` Use `--json` to get structured output: ```bash beeps relay create --name "Production Alerts" --external-key prod-alerts --description "Routes critical production incidents" --json ``` **Tool:** `beeps_create_relay` ```json { "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts" } ``` ## Listing Relays ```typescript const relays = await client.relay.list(); console.log(`Found ${relays.length} relays:`); relays.forEach((relay) => { console.log(`- ${relay.name} (${relay.id})`); }); ``` ```bash curl -X GET https://api.beeps.dev/v0/relay \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "relays": [ { "id": "rly_abc123", "organizationId": "org_xyz789", "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ] } ``` ```bash beeps relay list ``` Use `--json` to get structured output: ```bash beeps relay list --json ``` **Tool:** `beeps_list_relays` ```json {} ``` ## Input Types ### CreateRelayInput ```typescript type CreateRelayInput = { name: string; // Required: Human-readable relay name description?: string; // Optional: Relay description externalKey?: string; // Optional: Custom identifier for API access }; ``` ### Relay ```typescript type Relay = { id: string; // Unique relay identifier (e.g., "rly_abc123") organizationId: string; // Organization this relay belongs to name: string; // Relay name description: string; // Relay description externalKey: string | null; // Custom external identifier createdAt: string; // ISO 8601 timestamp updatedAt: string; // ISO 8601 timestamp }; ``` ## Best Practices Always set `externalKey` for idempotent configuration. See [Best Practices](/docs/best-practices/) for details. ## Error Handling The SDK throws errors by default. Use the `*Safe` methods for error handling without exceptions: ```typescript const result = await client.relay.createSafe({ name: "New Relay", }); if (result.error) { console.error(`Error: ${result.error.message}`); } else { console.log(`Success: ${result.data.id}`); } ``` For validation failures, the SDK formats a short human-readable message and also keeps structured field-level issues on `error.details`: ```typescript import { ValidationError } from "@beepsdev/sdk"; try { await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", ruleType: "schedule_notify", config: { schduleId: "sch_primary", // typo }, }); } catch (error) { if (error instanceof ValidationError) { console.error(error.message); console.error(error.details?.issues); } } ``` The message will look like: ```text Invalid config for relay rule `schedule_notify` - `config.scheduleId` is required. - `config.schduleId` Unknown field. Did you mean `scheduleId`? ``` --- # Relay Rules > Source: https://beeps.dev/docs/relays/rules/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Relay rules define how alerts are processed and routed. Rules execute in order based on their `order` property. ## Key Concepts ### Groups: Parallel vs Sequential Execution The `group` field controls whether rules run simultaneously or sequentially: - **Same group** = sequential (order 1, then 2, then 3) - **Different groups** = parallel (run at the same time) See [Best Practices - Parallel vs Sequential](/docs/best-practices/#parallel-vs-sequential-execution) for examples. ### Idempotency Set `externalKey` on relay rules to make configuration idempotent and safe to re-run. See [Best Practices](/docs/best-practices/#idempotent-configuration). ## Rule Types ### Schedule Notify Notify team members based on a schedule: ```typescript const rule = await client.relay.rules.create(relayId, { name: "Notify on-call engineer", ruleType: "schedule_notify", order: 1, config: { scheduleId: "sch_xyz123", }, enabled: true, }); ``` ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Notify on-call engineer", "ruleType": "schedule_notify", "order": 1, "config": { "scheduleId": "sch_xyz123" }, "enabled": true }' ``` ### Webhook Call an external webhook: ```typescript const rule = await client.relay.rules.create(relayId, { name: "Post to Slack", ruleType: "webhook", order: 2, config: { endpoint: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", method: "POST", headers: { "Content-Type": "application/json", }, payload: { text: "Alert received", severity: "high", }, timeout: 10000, // Optional: timeout in ms (default 30000) }, }); ``` ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Post to Slack", "ruleType": "webhook", "order": 2, "config": { "endpoint": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL", "method": "POST", "headers": { "Content-Type": "application/json" }, "payload": { "text": "Alert received" } } }' ``` ### AI Agent Invoke an AI agent to handle the alert: ```typescript const rule = await client.relay.rules.create(relayId, { name: "Invoke Devin agent", ruleType: "agent", order: 1, config: { agentType: "devin", // or "cursor" integrationId: "int_abc123", // References stored credentials endpoint: "https://api.devin.ai/v3/organizations/sessions", // Optional override config: { // Agent-specific configuration priority: "high", }, pollInterval: 30000, // Check status every 30s (default) maxPollAttempts: 120, // Give up after 120 attempts (default) }, }); ``` #### Cursor Agent Cursor requires a `repository` URL and can auto-create PRs: ```typescript const cursorRule = await client.relay.rules.create(relayId, { name: "Cursor Auto-Fix", ruleType: "agent", order: 1, config: { agentType: "cursor", integrationId: "int_cursor123", repository: "https://github.com/org/repo", // Required for Cursor autoCreatePr: true, // Default: true autoBranch: true, // Let Cursor auto-name the branch pollInterval: 30000, maxPollAttempts: 120, }, }); ``` ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Invoke Devin agent", "ruleType": "agent", "order": 1, "config": { "agentType": "devin", "integrationId": "int_abc123", "pollInterval": 30000, "maxPollAttempts": 120 } }' ``` #### Cursor Agent ```bash curl -X POST https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Cursor Auto-Fix", "ruleType": "agent", "order": 1, "config": { "agentType": "cursor", "integrationId": "int_cursor123", "repository": "https://github.com/org/repo", "autoCreatePr": true, "pollInterval": 30000, "maxPollAttempts": 120 } }' ``` ## Validation Errors Rule configs are validated strictly. If a required field is missing or a field name is misspelled, the SDK throws a `ValidationError` with a short message and field-level issues. ```typescript import { ValidationError } from "@beepsdev/sdk"; try { await client.relay.rules.create(relayId, { name: "Notify on-call engineer", ruleType: "schedule_notify", order: 1, config: { schduleId: "sch_xyz123", // typo }, }); } catch (error) { if (error instanceof ValidationError) { console.error(error.message); console.error(error.details?.issues); } } ``` Example SDK message: ```text Invalid config for relay rule `schedule_notify` - `config.scheduleId` is required. - `config.schduleId` Unknown field. Did you mean `scheduleId`? ``` The API returns the same validation failure as structured JSON: ```json { "error": { "code": "validation_failed", "message": "Invalid config for relay rule `schedule_notify`", "issues": [ { "path": ["config", "scheduleId"], "code": "missing_required", "message": "is required." }, { "path": ["config", "schduleId"], "code": "unknown_field", "message": "Unknown field.", "suggestion": "scheduleId" } ] } } ``` When present, the response also includes an `X-Request-ID` header you can include in support requests. ## Managing Rules ### List Rules ```typescript // List all rules const allRules = await client.relay.rules.list(relayId); // Filter by enabled status const enabledRules = await client.relay.rules.list(relayId, { enabled: true, }); // Filter by rule type const webhookRules = await client.relay.rules.list(relayId, { ruleType: "webhook", }); ``` ```bash # List all rules curl -X GET https://api.beeps.dev/v0/relay/rly_abc123/rules \ -H "Authorization: Bearer YOUR_API_KEY" # Filter by enabled status curl -X GET "https://api.beeps.dev/v0/relay/rly_abc123/rules?enabled=true" \ -H "Authorization: Bearer YOUR_API_KEY" # Filter by rule type curl -X GET "https://api.beeps.dev/v0/relay/rly_abc123/rules?ruleType=webhook" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Get a Specific Rule ```typescript const rule = await client.relay.rules.get(relayId, ruleId); console.log(`Rule: ${rule.name}, Order: ${rule.order}`); ``` ```bash curl -X GET https://api.beeps.dev/v0/relay/rly_abc123/rules/rul_xyz789 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Update a Rule ```typescript const updatedRule = await client.relay.rules.update(relayId, ruleId, { name: "Updated rule name", enabled: false, config: { // Updated config }, }); ``` ```bash curl -X PUT https://api.beeps.dev/v0/relay/rly_abc123/rules/rul_xyz789 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated rule name", "enabled": false }' ``` ### Delete a Rule ```typescript await client.relay.rules.delete(relayId, ruleId); console.log("Rule deleted successfully"); ``` ```bash curl -X DELETE https://api.beeps.dev/v0/relay/rly_abc123/rules/rul_xyz789 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Reorder Rules ```typescript const reorderedRules = await client.relay.rules.reorder(relayId, { rules: [ { id: "rul_first", order: 1 }, { id: "rul_second", order: 2 }, { id: "rul_third", order: 3 }, ], }); reorderedRules.forEach((rule) => { console.log(`${rule.order}: ${rule.name}`); }); ``` ```bash curl -X PUT https://api.beeps.dev/v0/relay/rly_abc123/rules/reorder \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "rules": [ {"id": "rul_first", "order": 1}, {"id": "rul_second", "order": 2}, {"id": "rul_third", "order": 3} ] }' ``` ## Rule Groups Organize related rules using the `group` property: ```typescript await client.relay.rules.create(relayId, { name: "Primary notification", ruleType: "schedule_notify", group: "primary", order: 1, config: { scheduleId: "sch_primary" }, }); await client.relay.rules.create(relayId, { name: "Backup notification", ruleType: "schedule_notify", group: "backup", order: 2, config: { scheduleId: "sch_backup" }, }); ``` ## Type Definitions ### CreateRelayRuleInput ```typescript type CreateRelayRuleInput = { externalKey?: string; name: string; ruleType: RelayRuleType; group?: string; order?: number; config: RelayRuleConfig; enabled?: boolean; }; ``` ### RelayRule ```typescript type RelayRule = { id: string; organizationId: string; relayId: string; group: string; externalKey: string | null; name: string; order: number; ruleType: RelayRuleType; config: Record; enabled: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ## Best Practices See [Best Practices](/docs/best-practices/) for relay rule configuration guidelines. --- # Relay API Reference > Source: https://beeps.dev/docs/relays/api/ Complete API reference for the Relay resource in the beeps SDK. ## SDK Client ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, baseURL: "https://api.beeps.dev", // Optional timeoutMs: 30000, // Optional: default 30s retries: { // Optional attempts: 3, backoffMs: 1000, }, }); ``` ## Relay Methods ### `client.relay.create(input)` Create a new relay. **Parameters:** - `input: CreateRelayInput` **Returns:** `Promise` **Example:** ```typescript const relay = await client.relay.create({ name: "Production Alerts", description: "Routes critical production incidents", externalKey: "prod-alerts", }); ``` ### `client.relay.list()` List all relays in your organization. **Returns:** `Promise` **Example:** ```typescript const relays = await client.relay.list(); ``` ### `client.relay.lint(relayId, input?)` Validate relay rules, schedules, and contact methods without side effects. **Parameters:** - `relayId: string` - `input?: RelayLintInput` **Returns:** `Promise` **Example:** ```typescript const result = await client.relay.lint("rly_abc123", { simulateAt: "2025-01-01T00:00:00Z", coverageDays: 7, }); if (result.issues.length > 0) { console.log(result.issues); } ``` ### `client.relay.simulate(relayId, input)` Preview relay execution without sending notifications. **Parameters:** - `relayId: string` - `input: RelaySimulationInput` **Returns:** `Promise` **Example:** ```typescript const result = await client.relay.simulate("rly_abc123", { simulateAt: "2025-01-01T00:00:00Z", payload: { action: "triggered", }, source: "sentry", }); console.log(result.groups); ``` ### `client.relay.createSafe(input)` Create a new relay without throwing exceptions. **Parameters:** - `input: CreateRelayInput` **Returns:** `Promise>` **Example:** ```typescript const result = await client.relay.createSafe({ name: "Production Alerts", }); if (result.error) { console.error(result.error.message); } else { console.log(result.data.id); } ``` For validation failures, `result.error` may be a `ValidationError` with field-level issues on `error.details?.issues`. ## Relay Rules Methods ### `client.relay.rules.create(relayId, input)` Create a new rule for a relay. **Parameters:** - `relayId: string` - `input: CreateRelayRuleInput` **Returns:** `Promise` **Example:** ```typescript const rule = await client.relay.rules.create("rly_abc123", { name: "Notify on-call", ruleType: "schedule_notify", order: 1, config: { scheduleId: "sch_xyz789", }, }); ``` If rule validation fails, the SDK throws a `ValidationError` with a formatted message and structured issues in `error.details?.issues`. The API returns the same issues in `error.issues` with code `validation_failed`. ### `client.relay.rules.list(relayId, params?)` List rules for a relay. **Parameters:** - `relayId: string` - `params?: ListRelayRulesParams` - `enabled?: boolean` - Filter by enabled status - `ruleType?: RelayRuleType` - Filter by rule type **Returns:** `Promise` **Example:** ```typescript const allRules = await client.relay.rules.list("rly_abc123"); const enabledRules = await client.relay.rules.list("rly_abc123", { enabled: true, }); ``` ### `client.relay.rules.get(relayId, ruleId)` Get a specific rule. **Parameters:** - `relayId: string` - `ruleId: string` **Returns:** `Promise` **Example:** ```typescript const rule = await client.relay.rules.get("rly_abc123", "rul_xyz789"); ``` ### `client.relay.rules.update(relayId, ruleId, input)` Update a rule. **Parameters:** - `relayId: string` - `ruleId: string` - `input: UpdateRelayRuleInput` **Returns:** `Promise` **Example:** ```typescript const updatedRule = await client.relay.rules.update( "rly_abc123", "rul_xyz789", { name: "Updated rule name", enabled: false, } ); ``` ### `client.relay.rules.delete(relayId, ruleId)` Delete a rule. **Parameters:** - `relayId: string` - `ruleId: string` **Returns:** `Promise<{ success: boolean }>` **Example:** ```typescript await client.relay.rules.delete("rly_abc123", "rul_xyz789"); ``` ### `client.relay.rules.reorder(relayId, input)` Reorder rules. **Parameters:** - `relayId: string` - `input: ReorderRelayRulesInput` **Returns:** `Promise` **Example:** ```typescript const reordered = await client.relay.rules.reorder("rly_abc123", { rules: [ { id: "rul_first", order: 1 }, { id: "rul_second", order: 2 }, ], }); ``` ## Safe Methods All mutation methods have corresponding `*Safe` variants that return `Result` instead of throwing: - `client.relay.lintSafe(relayId, input?)` - `client.relay.simulateSafe(relayId, input)` - `client.relay.rules.listSafe(relayId, params?)` - `client.relay.rules.createSafe(relayId, input)` - `client.relay.rules.getSafe(relayId, ruleId)` - `client.relay.rules.updateSafe(relayId, ruleId, input)` - `client.relay.rules.deleteSafe(relayId, ruleId)` - `client.relay.rules.reorderSafe(relayId, input)` ## Types ### RelayRuleType ```typescript type RelayRuleType = | "schedule_notify" | "webhook" | "agent" | "external_api" // deprecated | "wait" // deprecated | "conditional" // deprecated | "escalate"; // deprecated ``` ### ScheduleNotifyConfig ```typescript type ScheduleNotifyConfig = { scheduleId: string; notificationMethod?: "email" | "sms"; }; ``` ### WebhookConfig ```typescript type WebhookConfig = { endpoint: string; method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; headers?: Record; payload?: Record; timeout?: number; // Request timeout in ms (default 30000) }; ``` ### AgentConfig ```typescript type AgentConfig = { agentType: "devin" | "cursor"; integrationId?: string; // References stored credentials endpoint?: string; // Override endpoint if needed config?: Record; // Agent-specific config pollInterval?: number; // How often to poll in ms (default 30000) maxPollAttempts?: number; // Give up after N attempts (default 120) }; ``` ### RelayLintIssue ```typescript type RelayLintIssue = { severity: "error" | "warning" | "info"; code: string; message: string; relayId?: string; ruleId?: string; scheduleId?: string; details?: Record; }; ``` ### RelayLintResult ```typescript type RelayLintResult = { issues: RelayLintIssue[]; }; ``` ### RelaySimulationResult ```typescript type RelaySimulationResult = { relayId: string; simulateAt: string; alertPreview: { title: string; message?: string; severity: "critical" | "high" | "medium" | "low" | "info"; source: string; externalId?: string; metadata?: Record; }; groups: Array<{ groupName: string; steps: Array<{ ruleId: string; ruleType: "schedule_notify" | "webhook" | "agent" | string; name: string; enabled: boolean; }>; }>; issues: RelayLintIssue[]; }; ``` ## HTTP Endpoints ### Create Relay ``` POST /v0/relay ``` **Request Body:** ```json { "name": "Production Alerts", "description": "Routes critical production incidents", "externalKey": "prod-alerts" } ``` ### List Relays ``` GET /v0/relay ``` ### Create Relay Rule ``` POST /v0/relay/:relayId/rules ``` ### List Relay Rules ``` GET /v0/relay/:relayId/rules GET /v0/relay/:relayId/rules?enabled=true GET /v0/relay/:relayId/rules?ruleType=webhook ``` ### Lint Relay ``` POST /v0/relay/:relayId/lint ``` **Request Body:** ```json { "simulateAt": "2025-01-01T00:00:00Z", "coverageDays": 7 } ``` **Response:** ```json { "issues": [ { "severity": "warning", "code": "schedule.assignment_missing_contact_method", "message": "Schedule assignment has no verified contact method", "scheduleId": "sch_123", "details": { "userId": "usr_456" } } ] } ``` ### Simulate Relay ``` POST /v0/relay/:relayId/simulate ``` **Request Body:** ```json { "simulateAt": "2025-01-01T00:00:00Z", "payload": { "action": "triggered" }, "source": "sentry" } ``` **Response:** ```json { "relayId": "rly_123", "simulateAt": "2025-01-01T00:00:00Z", "alertPreview": { "title": "Test Alert", "severity": "medium", "source": "sentry" }, "groups": [], "issues": [] } ``` ### Get Relay Rule ``` GET /v0/relay/:relayId/rules/:ruleId ``` ### Update Relay Rule ``` PUT /v0/relay/:relayId/rules/:ruleId ``` ### Delete Relay Rule ``` DELETE /v0/relay/:relayId/rules/:ruleId ``` ### Reorder Relay Rules ``` PUT /v0/relay/:relayId/rules/reorder ``` ## Authentication All API requests require authentication using your API key: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.beeps.dev/v0/relay ``` --- # Schedules Overview > Source: https://beeps.dev/docs/schedules/overview/ Schedules define on-call rotations. Each schedule becomes active at `startAt`, then rotates team members on a daily or weekly cadence using `handoffDay` and `handoffTime`. If you omit `startAt` when creating a schedule, beeps starts it immediately. ## Schedule Types ### Daily Rotation Rotates every day at a specified time: ```typescript const schedule = await client.schedule.create({ name: "Daily On-Call", relayId: "rly_abc123", type: "daily", handoffDay: "monday", // Not used for daily schedules handoffTime: "09:00", // Rotates at 9:00 AM every day externalKey: "daily-oncall", // Idempotent - safe to re-run }); ``` ### Weekly Rotation Rotates every week on a specified day and time: ```typescript const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", type: "weekly", handoffDay: "monday", // Rotates every Monday handoffTime: "09:00", // at 9:00 AM externalKey: "weekly-oncall", // Idempotent - safe to re-run }); ``` ## Schedule Members Add team members to the schedule to participate in the rotation: ```typescript await client.schedule.addMember(schedule.id, { userId: "usr_alice", }); await client.schedule.addMember(schedule.id, { userId: "usr_bob", }); ``` Members rotate in the order they were added. New members usually join at the next handoff boundary so the current on-call assignment is not interrupted. If the schedule has not started yet, they become active at `startAt`. If the schedule is active but currently has no active members, they become active immediately. ## On-Call Status Check who is currently on-call: ```typescript const onCall = await client.schedule.getOnCall(schedule.id); console.log(`${onCall.email} is on-call`); ``` ## Assignments View upcoming or past assignments to understand the rotation: ```typescript // Get the next 14 days of assignments const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 14, }); assignments.forEach((assignment) => { console.log( `Assignment ${assignment.assignmentNumber}: ${assignment.userId} from ${assignment.startDate} to ${assignment.endDate}` ); }); ``` `assignmentNumber` is the generated assignment index for the schedule timeline. It is not a permanent member position. ## Schedule Start Set `startAt` when you want a schedule to begin in the future: ```typescript const schedule = await client.schedule.create({ name: "Planned Cutover", relayId: "rly_abc123", type: "weekly", handoffDay: "monday", handoffTime: "09:00", startAt: "2025-03-15T09:00:00.000Z", }); ``` If you omit `startAt`, the server defaults it to the current time. ## Schedule Overrides Overrides let you temporarily replace the on-call user without changing the rotation itself. They're useful for vacation coverage, sick days, or mid-shift handoffs. An override applies to a specific time window (up to 30 days) and expires automatically. While active, `getOnCall` returns the override user instead of the regularly scheduled person. Overrides cannot overlap on the same schedule. ```typescript const override = await client.schedule.createOverride(schedule.id, { userId: "usr_bob", startAt: "2025-03-15T09:00:00.000Z", endAt: "2025-03-22T09:00:00.000Z", reason: "Covering for Alice — vacation", }); ``` See [Schedule Overrides](/docs/schedules/overrides/) for the full management guide. ## Common Use Cases ### Basic On-Call Rotation Create a simple weekly rotation: ```typescript const relay = await client.relay.create({ name: "Production Alerts", }); const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); await client.schedule.addMember(schedule.id, { userId: "usr_alice" }); await client.schedule.addMember(schedule.id, { userId: "usr_bob" }); await client.schedule.addMember(schedule.id, { userId: "usr_charlie" }); await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", ruleType: "schedule_notify", config: { scheduleId: schedule.id, }, }); ``` ### Multi-Tier On-Call Create primary and backup schedules: ```typescript const primarySchedule = await client.schedule.create({ name: "Primary On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); const backupSchedule = await client.schedule.create({ name: "Backup On-Call", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); await client.relay.rules.create(relay.id, { name: "Notify primary first", order: 1, ruleType: "schedule_notify", config: { scheduleId: primarySchedule.id }, }); await client.relay.rules.create(relay.id, { name: "Escalate to backup", order: 2, ruleType: "schedule_notify", config: { scheduleId: backupSchedule.id }, }); ``` ### Follow-the-Sun Rotation Create different schedules for different time zones: ```typescript const usSchedule = await client.schedule.create({ name: "US Business Hours", relayId: relay.id, type: "daily", handoffDay: "monday", handoffTime: "08:00", // 8 AM US time }); const euSchedule = await client.schedule.create({ name: "EU Business Hours", relayId: relay.id, type: "daily", handoffDay: "monday", handoffTime: "08:00", // 8 AM EU time }); ``` --- # Managing Schedules > Source: https://beeps.dev/docs/schedules/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Create schedules and add team members to on-call rotations. ## Creating a Schedule ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", // Schedule must belong to a relay type: "weekly", // or "daily" handoffDay: "monday", // Day of week for weekly schedules handoffTime: "09:00", // Time when rotation occurs (HH:MM format) // startAt: "2025-03-15T09:00:00.000Z", // Optional: omit to start immediately externalKey: "prod-oncall", // Optional: custom identifier }); console.log(`Created schedule: ${schedule.id}`); ``` ### Daily Schedule ```typescript const dailySchedule = await client.schedule.create({ name: "Daily On-Call", relayId: "rly_abc123", type: "daily", handoffDay: "monday", // Not used for daily schedules handoffTime: "09:00", // Rotates every day at this time }); ``` ### Weekly Schedule ```typescript const weeklySchedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", type: "weekly", handoffDay: "friday", // Rotates every Friday handoffTime: "17:00", // at 5:00 PM }); ``` If you omit `startAt`, beeps starts the schedule immediately. Set it explicitly when you want the schedule to begin in the future. ```bash curl -X POST https://api.beeps.dev/v0/schedule \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Weekly On-Call", "relayId": "rly_abc123", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "externalKey": "prod-oncall" }' ``` **Response:** ```json { "schedule": { "id": "sch_xyz789", "organizationId": "org_abc123", "relayId": "rly_abc123", "name": "Weekly On-Call", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "startAt": "2025-01-15T10:30:00Z", "externalKey": "prod-oncall", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } } ``` Schedules are created via config-as-code. See [CLI Config-as-Code](/docs/cli/config/). ```bash beeps relay apply ``` **Tool:** `beeps_create_schedule` ```json { "name": "Weekly On-Call", "relayId": "rly_abc123", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00" } ``` ## Listing Schedules ```typescript const schedules = await client.schedule.list(); console.log(`Found ${schedules.length} schedules:`); schedules.forEach((schedule) => { console.log(`- ${schedule.name} (${schedule.type})`); }); ``` ```bash curl -X GET https://api.beeps.dev/v0/schedule \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "schedules": [ { "id": "sch_xyz789", "organizationId": "org_abc123", "relayId": "rly_abc123", "name": "Weekly On-Call", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "startAt": "2025-01-15T10:30:00Z", "externalKey": "prod-oncall", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } ] } ``` ```bash beeps schedule list ``` For JSON output: ```bash beeps schedule list --json ``` **Tool:** `beeps_list_schedules` ```json {} ``` ## Adding Schedule Members Members are added to the rotation in the order they're added. New members usually join at the next handoff boundary so the current on-call assignment is not interrupted. If the schedule has not started yet, they become active at `startAt`. If the schedule is active but currently has no active members, they become active immediately. You can add members by email address or user ID. ```typescript // Add by email address const member1 = await client.schedule.addMember(schedule.id, { email: "alice@example.com", }); console.log(`Added ${member1.userId} to schedule`); const member2 = await client.schedule.addMember(schedule.id, { email: "bob@example.com", }); console.log(`Added ${member2.userId} to schedule`); // Or add by user ID const member3 = await client.schedule.addMember(schedule.id, { userId: "usr_charlie", }); ``` ```bash # Add by email curl -X POST https://api.beeps.dev/v0/schedule/sch_xyz789/members \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com" }' # Or by user ID curl -X POST https://api.beeps.dev/v0/schedule/sch_xyz789/members \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_alice" }' ``` **Response:** ```json { "member": { "id": "scm_abc123", "scheduleId": "sch_xyz789", "userId": "usr_alice", "createdAt": "2025-01-15T10:35:00Z", "effectiveAt": "2025-01-20T09:00:00Z", "removedAt": null } } ``` **Tool:** `beeps_add_schedule_member` ```json { "scheduleId": "sch_xyz789", "email": "alice@example.com" } ``` ## Getting Current On-Call User ```typescript const onCall = await client.schedule.getOnCall(schedule.id); console.log(`Current on-call: ${onCall.email}`); console.log(`User ID: ${onCall.userId}`); console.log(`Assignment #: ${onCall.assignmentNumber}`); ``` ### Safe Method ```typescript const result = await client.schedule.getOnCallSafe(schedule.id); if (result.error) { console.error("Failed to get on-call:", result.error.message); } else { console.log(`On-call: ${result.data.email}`); } ``` ```bash curl -X GET https://api.beeps.dev/v0/schedule/sch_xyz789/on-call \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "onCall": { "userId": "usr_alice", "email": "alice@example.com", "assignmentNumber": 7, "scheduleId": "sch_xyz789" } } ``` ```bash beeps schedule on-call --schedule-id sch_xyz789 ``` For JSON output: ```bash beeps schedule on-call --schedule-id sch_xyz789 --json ``` **Tool:** `beeps_get_on_call` ```json { "scheduleId": "sch_xyz789" } ``` ## Type Definitions ### CreateScheduleInput ```typescript type CreateScheduleInput = { name: string; // Schedule name relayId: string; // Parent relay ID type: "daily" | "weekly"; // Rotation frequency handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; // Day of week (used for weekly schedules) handoffTime: string; // Time of day in HH:MM format (24-hour) startAt?: string | Date; // Optional: omit to start immediately externalKey?: string; // Optional custom identifier }; ``` ### Schedule ```typescript type Schedule = { id: string; organizationId: string; relayId: string; name: string; type: "daily" | "weekly"; handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; handoffTime: string; startAt: string; externalKey: string | null; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### AddScheduleMemberInput ```typescript type AddScheduleMemberInput = | { userId: string } // Add by user ID | { email: string }; // Add by email address ``` ### ScheduleMember ```typescript type ScheduleMember = { id?: string; scheduleId: string; userId: string; createdAt?: string; effectiveAt?: string; removedAt?: string | null; }; ``` ### BeepsUser ```typescript type BeepsUser = { userId: string; // User ID of the person currently on-call email: string; // Email address assignmentNumber: number | null; // Current generated assignment index (null when schedule has no members) scheduleId: string; // Schedule ID isOverride?: boolean; // True if user is on-call via schedule override overrideId?: string; // ID of the schedule override (if applicable) }; ``` ## Best Practices Always set `externalKey` for idempotent configuration. All times are in UTC. See [Best Practices](/docs/best-practices/#schedules) for details. ## Common Patterns ### Setting Up a New Team ```typescript const relay = await client.relay.create({ name: "Backend Team Alerts", }); const schedule = await client.schedule.create({ name: "Backend Weekly Rotation", relayId: relay.id, type: "weekly", handoffDay: "monday", handoffTime: "09:00", }); const teamEmails = ["alice@example.com", "bob@example.com", "charlie@example.com"]; for (const email of teamEmails) { await client.schedule.addMember(schedule.id, { email }); } await client.relay.rules.create(relay.id, { name: "Notify on-call engineer", ruleType: "schedule_notify", config: { scheduleId: schedule.id, }, }); ``` ### Checking Who's On Call ```typescript const onCall = await client.schedule.getOnCall(schedule.id); console.log(`${onCall.email} is currently on-call`); ``` --- # Schedule Overrides > Source: https://beeps.dev/docs/schedules/overrides/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Schedule overrides let you temporarily swap who is on-call. Use them for vacation coverage, sick days, or handing off to someone else mid-shift. An override takes effect for a specific time window and automatically expires when the window ends. Overrides cannot overlap with each other on the same schedule. The maximum duration is 30 days. ## Creating an Override ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); const override = await client.schedule.createOverride("sch_xyz789", { userId: "usr_bob", startAt: "2025-03-15T09:00:00.000Z", endAt: "2025-03-22T09:00:00.000Z", reason: "Alice on vacation, Bob covering", }); console.log(`Override created: ${override.id}`); console.log(`${override.userId} on-call from ${override.startAt} to ${override.endAt}`); ``` You can also pass `Date` objects: ```typescript const override = await client.schedule.createOverride("sch_xyz789", { userId: "usr_bob", startAt: new Date("2025-03-15T09:00:00.000Z"), endAt: new Date("2025-03-22T09:00:00.000Z"), reason: "Alice on vacation, Bob covering", }); ``` ```bash curl -X POST https://api.beeps.dev/v0/schedule/sch_xyz789/overrides \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering" }' ``` **Response:** ```json { "override": { "id": "ovr_abc123", "organizationId": "org_xyz789", "scheduleId": "sch_xyz789", "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering", "createdByUserId": "usr_alice", "createdAt": "2025-03-10T14:00:00Z", "updatedAt": "2025-03-10T14:00:00Z", "canceledAt": null, "deletedAt": null } } ``` ```bash beeps schedule override create \ --schedule-id sch_xyz789 \ --user-id usr_bob \ --start-at 2025-03-15T09:00:00.000Z \ --end-at 2025-03-22T09:00:00.000Z \ --reason "Alice on vacation, Bob covering" ``` Add `--json` for machine-readable output: ```bash beeps schedule override create \ --schedule-id sch_xyz789 \ --user-id usr_bob \ --start-at 2025-03-15T09:00:00.000Z \ --end-at 2025-03-22T09:00:00.000Z \ --reason "Alice on vacation, Bob covering" \ --json ``` **Tool:** `beeps_set_override` ```json { "scheduleId": "sch_xyz789", "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering" } ``` ## Listing Overrides ```typescript const overrides = await client.schedule.listOverrides("sch_xyz789"); overrides.forEach((override) => { console.log(`${override.userId}: ${override.startAt} - ${override.endAt}`); if (override.reason) { console.log(` Reason: ${override.reason}`); } }); ``` Filter by date range to find overrides within a specific window: ```typescript const overrides = await client.schedule.listOverrides("sch_xyz789", { startAt: "2025-03-01T00:00:00.000Z", endAt: "2025-03-31T23:59:59.999Z", }); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/overrides" \ -H "Authorization: Bearer YOUR_API_KEY" ``` With date range filtering: ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/overrides?startAt=2025-03-01T00:00:00.000Z&endAt=2025-03-31T23:59:59.999Z" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "overrides": [ { "id": "ovr_abc123", "scheduleId": "sch_xyz789", "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Alice on vacation, Bob covering", "createdByUserId": "usr_alice", "canceledAt": null } ] } ``` ```bash beeps schedule override list --schedule-id sch_xyz789 ``` With date range filtering: ```bash beeps schedule override list \ --schedule-id sch_xyz789 \ --start-at 2025-03-01T00:00:00.000Z \ --end-at 2025-03-31T23:59:59.999Z ``` Add `--json` for machine-readable output: ```bash beeps schedule override list --schedule-id sch_xyz789 --json ``` ## Updating an Override Change the time window or reason for an existing override. All fields are optional—only include what you want to change. ```typescript const updated = await client.schedule.updateOverride( "sch_xyz789", "ovr_abc123", { endAt: "2025-03-25T09:00:00.000Z", reason: "Extended coverage — Alice returning Thursday", }, ); console.log(`Override now ends at ${updated.endAt}`); ``` ```bash curl -X PATCH https://api.beeps.dev/v0/schedule/sch_xyz789/overrides/ovr_abc123 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage — Alice returning Thursday" }' ``` **Response:** ```json { "override": { "id": "ovr_abc123", "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage — Alice returning Thursday", "updatedAt": "2025-03-12T10:00:00Z" } } ``` ```bash beeps schedule override update \ --schedule-id sch_xyz789 \ --override-id ovr_abc123 \ --end-at 2025-03-25T09:00:00.000Z \ --reason "Extended coverage" ``` Add `--json` for machine-readable output: ```bash beeps schedule override update \ --schedule-id sch_xyz789 \ --override-id ovr_abc123 \ --end-at 2025-03-25T09:00:00.000Z \ --reason "Extended coverage" \ --json ``` **Tool:** `beeps_update_override` ```json { "scheduleId": "sch_xyz789", "overrideId": "ovr_abc123", "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage" } ``` ## Canceling an Override Cancel an override to restore the normal rotation. The on-call user reverts to whoever the schedule assigns. ```typescript const canceled = await client.schedule.cancelOverride("sch_xyz789", "ovr_abc123"); console.log(`Override canceled at ${canceled.canceledAt}`); ``` ```bash curl -X DELETE https://api.beeps.dev/v0/schedule/sch_xyz789/overrides/ovr_abc123 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "override": { "id": "ovr_abc123", "canceledAt": "2025-03-12T10:00:00Z" } } ``` ```bash beeps schedule override cancel --schedule-id sch_xyz789 --override-id ovr_abc123 ``` Add `--json` for machine-readable output: ```bash beeps schedule override cancel --schedule-id sch_xyz789 --override-id ovr_abc123 --json ``` **Tool:** `beeps_cancel_override` ```json { "scheduleId": "sch_xyz789", "overrideId": "ovr_abc123" } ``` ## Type Definitions ### CreateScheduleOverrideInput ```typescript type CreateScheduleOverrideInput = { userId: string; startAt: string | Date; endAt: string | Date; reason?: string; }; ``` ### UpdateScheduleOverrideInput ```typescript type UpdateScheduleOverrideInput = { startAt?: string | Date; endAt?: string | Date; reason?: string; }; ``` ### ListScheduleOverridesParams ```typescript type ListScheduleOverridesParams = { startAt?: string | Date; endAt?: string | Date; }; ``` ### ScheduleOverride ```typescript type ScheduleOverride = { id: string; organizationId: string; scheduleId: string; userId: string; startAt: string; endAt: string; reason: string | null; createdByUserId: string; createdAt: string; updatedAt: string; canceledAt: string | null; deletedAt: string | null; }; ``` ## Common Patterns ### Vacation Coverage ```typescript const vacationStart = new Date("2025-06-01T00:00:00.000Z"); const vacationEnd = new Date("2025-06-14T00:00:00.000Z"); const override = await client.schedule.createOverride(schedule.id, { userId: "usr_bob", startAt: vacationStart, endAt: vacationEnd, reason: "Covering for Alice — vacation June 1-14", }); ``` ### Temporary Handoff ```typescript const now = new Date(); const fourHoursLater = new Date(now.getTime() + 4 * 60 * 60 * 1000); const override = await client.schedule.createOverride(schedule.id, { userId: "usr_charlie", startAt: now, endAt: fourHoursLater, reason: "Handing off for a few hours", }); ``` ### Checking Who Is Actually On-Call The `getOnCall` method already accounts for overrides. If an override is active, the returned user will have `isOverride: true` and `overrideId` set: ```typescript const onCall = await client.schedule.getOnCall(schedule.id); if (onCall.isOverride) { console.log(`${onCall.email} is on-call via override ${onCall.overrideId}`); } else { console.log(`${onCall.email} is on-call (assignment #${onCall.assignmentNumber})`); } ``` --- # Schedule Assignments > Source: https://beeps.dev/docs/schedules/assignments/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Assignments show who is on-call during specific time periods. Use assignments to plan coverage, understand rotation patterns, and track historical data. ## Understanding Assignments Each assignment represents: - **Assignment Number**: The generated assignment index since the schedule started - **User ID**: Who is on-call for this assignment - **Start Date**: When this assignment begins (ISO 8601 format) - **End Date**: When this assignment ends (ISO 8601 format) ## Viewing Assignments You can retrieve assignments in three ways: ### By Number of Assignments Get a specific number of upcoming assignments: ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "assignments", count: 5, // Get next 5 assignments }); assignments.forEach((assignment) => { console.log( `Assignment ${assignment.assignmentNumber}: ${assignment.userId}` ); console.log(` ${assignment.startDate} → ${assignment.endDate}`); }); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/assignments?type=assignments&count=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps schedule assignments --schedule-id sch_xyz789 --count 5 ``` For JSON output: ```bash beeps schedule assignments --schedule-id sch_xyz789 --count 5 --json ``` **Tool:** `beeps_get_schedule_assignments` ```json { "scheduleId": "sch_xyz789", "count": 5 } ``` ### By Number of Days Get all assignments covering a specific number of days: ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 14, // Get assignments for next 14 days }); console.log(`Assignments covering next 14 days: ${assignments.length}`); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/assignments?type=days&count=14" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps schedule assignments --schedule-id sch_xyz789 --type days --count 14 ``` For JSON output: ```bash beeps schedule assignments --schedule-id sch_xyz789 --type days --count 14 --json ``` **Tool:** `beeps_get_schedule_assignments` ```json { "scheduleId": "sch_xyz789", "type": "days", "count": 14 } ``` ### Until a Specific Date Get all assignments up to a specific date: ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "until", date: "2025-12-31T23:59:59.999Z", // Get assignments until end of year }); console.log(`Assignments until 2025-12-31: ${assignments.length}`); ``` ```bash curl -X GET "https://api.beeps.dev/v0/schedule/sch_xyz789/assignments?type=until&date=2025-12-31T23:59:59.999Z" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps schedule assignments --schedule-id sch_xyz789 --type until --date 2025-12-31T23:59:59.999Z ``` For JSON output: ```bash beeps schedule assignments --schedule-id sch_xyz789 --type until --date 2025-12-31T23:59:59.999Z --json ``` **Tool:** `beeps_get_schedule_assignments` ```json { "scheduleId": "sch_xyz789", "type": "until", "date": "2025-12-31T23:59:59.999Z" } ``` ## API Response ```json { "assignments": [ { "userId": "usr_alice", "startDate": "2025-01-20T09:00:00Z", "endDate": "2025-01-27T09:00:00Z", "assignmentNumber": 7 }, { "userId": "usr_bob", "startDate": "2025-01-27T09:00:00Z", "endDate": "2025-02-03T09:00:00Z", "assignmentNumber": 8 }, { "userId": "usr_charlie", "startDate": "2025-02-03T09:00:00Z", "endDate": "2025-02-10T09:00:00Z", "assignmentNumber": 9 } ] } ``` ## Type Definitions ### GetAssignmentsParams ```typescript type GetAssignmentsParams = | { type?: "assignments"; // Default count?: number; // Number of assignments (optional) } | { type: "until"; date: string | Date; // ISO 8601 date or JavaScript Date } | { type: "days"; count: number; // Number of days }; ``` ### ScheduleAssignment ```typescript type ScheduleAssignment = { userId: string; // User on-call for this assignment startDate: string; // ISO 8601 timestamp endDate: string; // ISO 8601 timestamp assignmentNumber: number; // Generated assignment index since schedule start }; ``` ## Common Use Cases ### Show Who's On-Call This Week ```typescript const onCall = await client.schedule.getOnCall(schedule.id); const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 7, }); const currentAssignment = assignments.find( (a) => a.assignmentNumber === onCall.assignmentNumber ); if (currentAssignment) { console.log(`${onCall.email} is on-call until ${currentAssignment.endDate}`); } ``` ### Display Rotation Calendar ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 30, }); console.log("On-Call Calendar (Next 30 Days):\n"); assignments.forEach((assignment) => { const start = new Date(assignment.startDate).toLocaleDateString(); const end = new Date(assignment.endDate).toLocaleDateString(); console.log(`${start} - ${end}: ${assignment.userId}`); }); ``` ### Find Next Assignment for a User ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "assignments", count: 10, }); const userId = "usr_alice"; const nextAssignment = assignments.find((a) => a.userId === userId); if (nextAssignment) { console.log(`${userId}'s next on-call starts: ${nextAssignment.startDate}`); } else { console.log(`${userId} not found in next 10 assignments`); } ``` ### Calculate On-Call Load ```typescript const assignments = await client.schedule.getAssignments(schedule.id, { type: "days", count: 90, }); const userCounts = new Map(); assignments.forEach((assignment) => { const count = userCounts.get(assignment.userId) || 0; userCounts.set(assignment.userId, count + 1); }); console.log("On-Call Load (Next 90 Days):"); userCounts.forEach((count, userId) => { console.log(`${userId}: ${count} assignments`); }); ``` ### Vacation Planning ```typescript function formatDate(date: Date): string { return date.toISOString().split("T")[0]; } const vacationStart = new Date("2025-03-15"); const vacationEnd = new Date("2025-03-22"); const assignments = await client.schedule.getAssignments(schedule.id, { type: "until", date: formatDate(vacationEnd), }); const conflictingAssignments = assignments.filter((assignment) => { const start = new Date(assignment.startDate); const end = new Date(assignment.endDate); return ( assignment.userId === "usr_alice" && start <= vacationEnd && end >= vacationStart ); }); if (conflictingAssignments.length > 0) { console.log("⚠️ On-call assignments during vacation:"); conflictingAssignments.forEach((a) => { console.log(` ${a.startDate} → ${a.endDate}`); }); } else { console.log("✓ No conflicts with vacation dates"); } ``` ## Best Practices All dates are in UTC—convert to local time zones for display. Cache assignments to reduce API calls since they don't change frequently. --- # Schedule API Reference > Source: https://beeps.dev/docs/schedules/api/ Complete API reference for the Schedule resource in the beeps SDK. If you omit `startAt` when creating a schedule, the server defaults it to the current time so the schedule starts immediately. ## Schedule Methods ### `client.schedule.create(input)` Create a new schedule. **Parameters:** - `input: CreateScheduleInput` **Returns:** `Promise` **Example:** ```typescript const schedule = await client.schedule.create({ name: "Weekly On-Call", relayId: "rly_abc123", type: "weekly", handoffDay: "monday", handoffTime: "09:00", externalKey: "prod-oncall", }); ``` ### `client.schedule.list()` List all schedules in your organization. **Returns:** `Promise` **Example:** ```typescript const schedules = await client.schedule.list(); ``` ### `client.schedule.addMember(scheduleId, input)` Add a team member to a schedule rotation. New members usually join at the next handoff boundary so the current on-call assignment is not interrupted. If the schedule has not started yet, they become active at `startAt`. If the schedule is active but currently has no active members, they become active immediately. **Parameters:** - `scheduleId: string` - `input: AddScheduleMemberInput` **Returns:** `Promise` **Examples:** ```typescript // Add by email address const member = await client.schedule.addMember("sch_xyz789", { email: "alice@example.com", }); // Or add by user ID const member = await client.schedule.addMember("sch_xyz789", { userId: "usr_alice", }); // Alice joins based on the schedule timing ``` ### `client.schedule.listMembers(scheduleId)` List active members of a schedule rotation. **Parameters:** - `scheduleId: string` **Returns:** `Promise` **Example:** ```typescript const members = await client.schedule.listMembers("sch_xyz789"); ``` ### `client.schedule.listMembersSafe(scheduleId)` List active schedule members without throwing exceptions. **Parameters:** - `scheduleId: string` **Returns:** `Promise>` **Example:** ```typescript const result = await client.schedule.listMembersSafe("sch_xyz789"); if (result.error) { console.error("Error:", result.error.message); } else { console.log(`Members: ${result.data.length}`); } ``` ### `client.schedule.removeMember(scheduleId, userId)` Remove a team member from a schedule rotation. The member will remain on-call through the end of their current shift, then be removed at the next rotation boundary. Cannot remove a member if it would leave the schedule with no active members. **Parameters:** - `scheduleId: string` - `userId: string` **Returns:** `Promise` **Example:** ```typescript const member = await client.schedule.removeMember("sch_xyz789", "usr_alice"); // Alice completes her current shift, then leaves the rotation ``` ### `client.schedule.getAssignments(scheduleId, params?)` Get schedule assignments. **Parameters:** - `scheduleId: string` - `params?: GetAssignmentsParams` **Returns:** `Promise` **Examples:** ```typescript // Get next 5 assignments const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "assignments", count: 5, }); // Get assignments for next 14 days const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "days", count: 14, }); // Get assignments until specific date (using Date object) const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "until", date: new Date("2025-12-31T23:59:59.999Z"), }); // Or using ISO 8601 string with Z suffix (UTC timezone required) const assignments = await client.schedule.getAssignments("sch_xyz789", { type: "until", date: "2025-12-31T23:59:59.999Z", }); ``` ### `client.schedule.getOnCall(scheduleId)` Get the user currently on-call for a schedule. **Parameters:** - `scheduleId: string` **Returns:** `Promise` **Example:** ```typescript const onCall = await client.schedule.getOnCall("sch_xyz789"); console.log(`${onCall.email} is on-call`); ``` ### `client.schedule.getOnCallSafe(scheduleId)` Get the user currently on-call without throwing exceptions. **Parameters:** - `scheduleId: string` **Returns:** `Promise>` **Example:** ```typescript const result = await client.schedule.getOnCallSafe("sch_xyz789"); if (result.error) { console.error("Error:", result.error.message); } else { console.log(`On-call: ${result.data.email}`); } ``` ## Override Methods ### `client.schedule.createOverride(scheduleId, input)` Create a temporary on-call override. The override user takes over for the specified time window. **Parameters:** - `scheduleId: string` - `input: CreateScheduleOverrideInput` **Returns:** `Promise` **Example:** ```typescript const override = await client.schedule.createOverride("sch_xyz789", { userId: "usr_bob", startAt: "2025-03-15T09:00:00.000Z", endAt: "2025-03-22T09:00:00.000Z", reason: "Vacation coverage", }); ``` ### `client.schedule.listOverrides(scheduleId, params?)` List overrides for a schedule, optionally filtered by date range. **Parameters:** - `scheduleId: string` - `params?: ListScheduleOverridesParams` **Returns:** `Promise` **Example:** ```typescript const overrides = await client.schedule.listOverrides("sch_xyz789"); // With date range filter const overrides = await client.schedule.listOverrides("sch_xyz789", { startAt: "2025-03-01T00:00:00.000Z", endAt: "2025-03-31T23:59:59.999Z", }); ``` ### `client.schedule.updateOverride(scheduleId, overrideId, input)` Update an existing override. All fields are optional. **Parameters:** - `scheduleId: string` - `overrideId: string` - `input: UpdateScheduleOverrideInput` **Returns:** `Promise` **Example:** ```typescript const updated = await client.schedule.updateOverride("sch_xyz789", "ovr_abc123", { endAt: "2025-03-25T09:00:00.000Z", reason: "Extended coverage", }); ``` ### `client.schedule.cancelOverride(scheduleId, overrideId)` Cancel an override. The normal rotation resumes. **Parameters:** - `scheduleId: string` - `overrideId: string` **Returns:** `Promise` **Example:** ```typescript const canceled = await client.schedule.cancelOverride("sch_xyz789", "ovr_abc123"); ``` ## Types ### CreateScheduleInput ```typescript type CreateScheduleInput = { name: string; relayId: string; type: "daily" | "weekly"; handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; handoffTime: string; // HH:MM format (24-hour) startAt?: string | Date; externalKey?: string; }; ``` ### Schedule ```typescript type Schedule = { id: string; organizationId: string; relayId: string; name: string; type: "daily" | "weekly"; handoffDay: | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday"; handoffTime: string; startAt: string; externalKey: string | null; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### AddScheduleMemberInput ```typescript type AddScheduleMemberInput = | { userId: string } | { email: string }; ``` ### ScheduleMember ```typescript type ScheduleMember = { id?: string; scheduleId: string; userId: string; createdAt?: string; effectiveAt?: string; removedAt?: string | null; }; ``` ### GetAssignmentsParams ```typescript type GetAssignmentsParams = | { type?: "assignments"; count?: number } | { type: "until"; date: string | Date } | { type: "days"; count: number }; ``` **Note:** When using `type: "until"`, the `date` parameter must be either: - A JavaScript `Date` object (automatically converted to UTC ISO 8601 format) - An ISO 8601 timestamp string with Z suffix (e.g., `"2025-12-31T23:59:59.999Z"`) The SDK validates that string dates include the Z suffix to ensure UTC timezone consistency. Dates without timezone indicators will be rejected with a clear error message. ### ScheduleAssignment ```typescript type ScheduleAssignment = { userId: string; startDate: string; // ISO 8601 timestamp endDate: string; // ISO 8601 timestamp assignmentNumber: number; }; ``` ### BeepsUser ```typescript type BeepsUser = { userId: string; email: string; assignmentNumber: number | null; // Generated assignment index, null when schedule has no members scheduleId: string; isOverride?: boolean; // True if user is on-call via schedule override overrideId?: string; // ID of the schedule override (if applicable) }; ``` ### CreateScheduleOverrideInput ```typescript type CreateScheduleOverrideInput = { userId: string; startAt: string | Date; endAt: string | Date; reason?: string; }; ``` ### UpdateScheduleOverrideInput ```typescript type UpdateScheduleOverrideInput = { startAt?: string | Date; endAt?: string | Date; reason?: string; }; ``` ### ListScheduleOverridesParams ```typescript type ListScheduleOverridesParams = { startAt?: string | Date; endAt?: string | Date; }; ``` ### ScheduleOverride ```typescript type ScheduleOverride = { id: string; organizationId: string; scheduleId: string; userId: string; startAt: string; endAt: string; reason: string | null; createdByUserId: string; createdAt: string; updatedAt: string; canceledAt: string | null; deletedAt: string | null; }; ``` ## HTTP Endpoints ### Create Schedule ``` POST /v0/schedule ``` **Request Body:** ```json { "name": "Weekly On-Call", "relayId": "rly_abc123", "type": "weekly", "handoffDay": "monday", "handoffTime": "09:00", "externalKey": "prod-oncall" } ``` If `startAt` is omitted, the server sets it to the current time. ### List Schedules ``` GET /v0/schedule ``` ### Add Schedule Member ``` POST /v0/schedule/:scheduleId/members ``` Adds a member to the rotation. They will join at the next rotation boundary. Pass either `userId` or `email`. **Request Body:** ```json { "email": "alice@example.com" } ``` Or by user ID: ```json { "userId": "usr_alice" } ``` ### List Schedule Members ``` GET /v0/schedule/:scheduleId/members ``` ### Remove Schedule Member ``` DELETE /v0/schedule/:scheduleId/members/:userId ``` Removes a member from the rotation. They will complete their current shift and leave at the next rotation boundary. Cannot remove a member if it would leave the schedule with no active members. ### Get Assignments ``` GET /v0/schedule/:scheduleId/assignments GET /v0/schedule/:scheduleId/assignments?type=assignments&count=5 GET /v0/schedule/:scheduleId/assignments?type=days&count=14 GET /v0/schedule/:scheduleId/assignments?type=until&date=2025-12-31T23:59:59.999Z ``` **Note:** The `date` parameter for `type=until` must be a valid ISO 8601 timestamp with Z suffix (UTC timezone). Dates without the Z suffix will be rejected with a validation error. ### Get Current On-Call User ``` GET /v0/schedule/:scheduleId/on-call ``` ### Create Override ``` POST /v0/schedule/:scheduleId/overrides ``` **Request Body:** ```json { "userId": "usr_bob", "startAt": "2025-03-15T09:00:00.000Z", "endAt": "2025-03-22T09:00:00.000Z", "reason": "Vacation coverage" } ``` **Note:** `startAt` and `endAt` must be valid ISO 8601 timestamps with Z suffix (UTC timezone). ### List Overrides ``` GET /v0/schedule/:scheduleId/overrides GET /v0/schedule/:scheduleId/overrides?startAt=2025-03-01T00:00:00.000Z&endAt=2025-03-31T23:59:59.999Z ``` ### Update Override ``` PATCH /v0/schedule/:scheduleId/overrides/:overrideId ``` **Request Body:** ```json { "endAt": "2025-03-25T09:00:00.000Z", "reason": "Extended coverage" } ``` ### Cancel Override ``` DELETE /v0/schedule/:scheduleId/overrides/:overrideId ``` ## Authentication All API requests require authentication using your API key: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.beeps.dev/v0/schedule ``` --- # Contact Methods Overview > Source: https://beeps.dev/docs/contact-methods/overview/ Contact methods define how users receive notifications. Each user can have multiple email and SMS contact methods. When a relay rule notifies an on-call user, it uses their configured contact methods. ## Automatic Email Contact Method When you sign up for beeps, your signup email is automatically added as a verified contact method. This means you'll start receiving notifications immediately without any extra setup. ## Managing Contact Methods Contact methods are managed through the beeps web UI at **Settings > Profile**. From there you can: - Add additional email addresses - Add SMS phone numbers (E.164 format, e.g. `+14155551234`) - Remove contact methods you no longer need - Resend verification for unverified SMS numbers ## Transport Types ### Email Email contact methods are automatically verified when added. ### SMS SMS contact methods require verification. After adding a phone number, you'll receive a text message and must reply **YES** to confirm. ## Verification Status Contact methods have a `verified` status: - **Verified**: Confirmed to be working and deliverable - **Unverified**: Not yet confirmed Unverified contact methods receive notifications, but delivery is not guaranteed. Verify methods for reliable delivery. ## Listing Contact Methods via SDK You can list and delete contact methods programmatically: ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); console.log(`Alice has ${methods.length} contact methods`); const emailMethods = methods.filter((m) => m.transport === "email"); const smsMethods = methods.filter((m) => m.transport === "sms"); console.log(`Emails: ${emailMethods.length}, SMS: ${smsMethods.length}`); ``` ### Cleaning Up Old Contact Methods Remove outdated contact information: ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); const oldEmail = methods.find( (m) => m.value === "old-email@example.com" ); if (oldEmail) { await client.contactMethod.delete(oldEmail.id, { userId: "usr_alice", }); } ``` ## Best Practices See [Best Practices - Contact Methods](/docs/best-practices/#contact-methods) for configuration guidelines. --- # Managing Contact Methods > Source: https://beeps.dev/docs/contact-methods/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Contact methods are created and managed through the beeps web UI at **Settings > Profile**. Your signup email is automatically added as a verified contact method. The SDK and API provide read and delete access for programmatic management. ## Listing Contact Methods ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); console.log(`Found ${methods.length} contact methods`); methods.forEach((method) => { console.log(`- ${method.transport}: ${method.value}`); console.log(` Verified: ${method.verified}`); }); ``` ### Filter by Transport Type ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); const emailMethods = methods.filter((m) => m.transport === "email"); const smsMethods = methods.filter((m) => m.transport === "sms"); console.log(`Emails: ${emailMethods.length}`); console.log(`SMS: ${smsMethods.length}`); ``` ```bash curl -X GET "https://api.beeps.dev/v0/contact-methods?userId=usr_alice" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "contactMethods": [ { "id": "cm_abc123", "userId": "usr_alice", "transport": "email", "value": "alice@example.com", "verified": true, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null }, { "id": "cm_def456", "userId": "usr_alice", "transport": "sms", "value": "+14155551234", "verified": false, "createdAt": "2025-01-15T11:00:00Z", "updatedAt": "2025-01-15T11:00:00Z", "deletedAt": null } ] } ``` ## Deleting a Contact Method ```typescript const result = await client.contactMethod.delete("cm_abc123", { userId: "usr_alice", }); console.log("Deleted successfully:", result.success); ``` ### Safe Method ```typescript const result = await client.contactMethod.deleteSafe("cm_abc123", { userId: "usr_alice", }); if (result.error) { console.error("Failed to delete:", result.error.message); } else { console.log("Deleted:", result.data.success); } ``` ```bash curl -X DELETE "https://api.beeps.dev/v0/contact-methods/cm_abc123?userId=usr_alice" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "success": true } ``` ## Type Definitions ### ContactMethod ```typescript type ContactMethod = { id: string; userId: string; transport: string; value: string; verified: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### ListContactMethodsParams ```typescript type ListContactMethodsParams = { userId: string; }; ``` ### DeleteContactMethodParams ```typescript type DeleteContactMethodParams = { userId: string; }; ``` ## Troubleshooting **No one receiving notifications?** See [Troubleshooting - No One Is Notified](/docs/troubleshooting/#alerts-appear-but-no-one-is-notified). --- # Contact Methods API Reference > Source: https://beeps.dev/docs/contact-methods/api/ Complete API reference for the Contact Method resource in the beeps SDK. ## Contact Method Methods ### `client.contactMethod.list(params)` List all contact methods for a user. **Parameters:** - `params: ListContactMethodsParams` **Returns:** `Promise` **Example:** ```typescript const methods = await client.contactMethod.list({ userId: "usr_alice", }); ``` ### `client.contactMethod.delete(id, params)` Delete a contact method. **Parameters:** - `id: string` - Contact method ID - `params: DeleteContactMethodParams` **Returns:** `Promise<{ success: boolean }>` **Example:** ```typescript await client.contactMethod.delete("cm_abc123", { userId: "usr_alice", }); ``` ## Safe Methods All methods have corresponding `*Safe` variants that return `Result` instead of throwing: ### `client.contactMethod.listSafe(params)` **Returns:** `Promise>` ### `client.contactMethod.deleteSafe(id, params)` **Returns:** `Promise>` ## Types ### ContactMethodTransport ```typescript type ContactMethodTransport = "email" | "sms"; ``` ### ContactMethod ```typescript type ContactMethod = { id: string; userId: string; transport: string; value: string; verified: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ### ListContactMethodsParams ```typescript type ListContactMethodsParams = { userId: string; }; ``` ### DeleteContactMethodParams ```typescript type DeleteContactMethodParams = { userId: string; }; ``` ## HTTP Endpoints ### List Contact Methods ``` GET /v0/contact-methods?userId=usr_alice ``` **Query Parameters:** - `userId` (required): User ID to list contact methods for ### Delete Contact Method ``` DELETE /v0/contact-methods/:id?userId=usr_alice ``` **Query Parameters:** - `userId` (required): User ID (for authorization) ## Authentication All API requests require authentication using your API key: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.beeps.dev/v0/contact-methods?userId=usr_alice ``` --- # Alerts Overview > Source: https://beeps.dev/docs/alerts/overview/ Alerts are incidents received via webhooks from your monitoring systems. They have a severity level (critical, high, medium, low, info), flow through relays, and can be responded to, assigned, and resolved. ## Alert Lifecycle 1. **Created**: Alert is received via webhook or integration 2. **Routed**: Alert flows through relay rules 3. **Notified**: On-call users are notified via their contact methods 4. **Responded**: One or more responders signal they're on it 5. **Assigned**: Alert is assigned to a specific user (optional) 6. **Resolved**: Alert is marked as resolved Multiple responders can join the same alert. Each responder tracks their own status (`on_it`, `done`, `dropped`) independently. ## Alert Severity Levels Alerts have five severity levels: - **Critical**: Immediate attention required, service down - **High**: Important issue requiring prompt attention - **Medium**: Non-urgent issue that should be addressed - **Low**: Minor issue with minimal impact - **Info**: Informational notification, no action required ```typescript const alerts = await client.alert.list(); const criticalAlerts = alerts.filter((a) => a.severity === "critical"); console.log(`${criticalAlerts.length} critical alerts`); ``` ## Alert States ### Active Alerts Alerts that haven't been resolved: ```typescript const activeAlerts = await client.alert.listActive(); console.log(`${activeAlerts.length} alerts need attention`); ``` ### Resolved Alerts Alerts that have been resolved: ```typescript const resolvedAlerts = await client.alert.listResolved(); console.log(`${resolvedAlerts.length} alerts resolved`); ``` ## Common Operations ### Responding to an Alert Signal that you're working on an alert: ```typescript const responder = await client.alert.onIt("alr_abc123", { userId: "usr_alice", }); console.log(`${responder.userId} is on it (joined ${responder.joinedAt})`); ``` ### Assigning an Alert Assign an alert to a specific user: ```typescript const assigned = await client.alert.assign("alr_abc123", "usr_bob"); console.log(`Assigned to ${assigned.assignedToUserId}`); ``` ### Resolving an Alert Mark an alert as resolved: ```typescript const resolved = await client.alert.resolve("alr_abc123"); console.log(`Resolved at ${resolved.resolvedAt}`); ``` ## Alert Metadata Alerts can include arbitrary metadata for context: ```typescript const alert = await client.alert.get("alr_abc123"); console.log("Alert metadata:", alert.metadata); // Example: { server: "web-01", region: "us-west", errorRate: 15.3 } ``` ## Common Use Cases ### Monitoring Active Alerts ```typescript const active = await client.alert.listActive(); console.log(`Active Alerts: ${active.length}\n`); active.forEach((alert) => { console.log(`[${alert.severity.toUpperCase()}] ${alert.title}`); console.log(` Source: ${alert.source}`); if (alert.assignedToUserId) { console.log(` Assigned to: ${alert.assignedToUserId}`); } console.log(); }); ``` ### On-Call Dashboard ```typescript const active = await client.alert.listActive(); const byUser = new Map(); active.forEach((alert) => { if (alert.assignedToUserId) { const count = byUser.get(alert.assignedToUserId) || 0; byUser.set(alert.assignedToUserId, count + 1); } }); console.log("Alerts by User:"); byUser.forEach((count, userId) => { console.log(`${userId}: ${count} alerts`); }); ``` ### Alert Workflow ```typescript const alertId = "alr_abc123"; const userId = "usr_alice"; const alert = await client.alert.get(alertId); console.log(`Working on: ${alert.title}`); const responder = await client.alert.onIt(alertId, { userId }); console.log("On it"); await client.alert.assign(alertId, userId); console.log("Assigned to self"); console.log("Investigating..."); await client.alert.updateResponderStatus(alertId, responder.id, { status: "done", }); console.log("Responder done"); await client.alert.resolve(alertId); console.log("Resolved"); ``` --- # Managing Alerts > Source: https://beeps.dev/docs/alerts/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Manage alerts throughout their lifecycle. ## Listing All Alerts ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); const alerts = await client.alert.list(); console.log(`Found ${alerts.length} total alerts`); alerts.forEach((alert) => { console.log(`[${alert.severity}] ${alert.title}`); }); ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "alerts": [ { "id": "alr_abc123", "organizationId": "org_xyz789", "webhookId": "wh_def456", "title": "High CPU usage on web-01", "message": "CPU usage has exceeded 90% for 5 minutes", "severity": "high", "source": "monitoring", "externalId": "mon-12345", "metadata": { "server": "web-01", "cpu": 92.5 }, "assignedToUserId": null, "resolvedAt": null, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } ] } ``` ```bash beeps alert list beeps alert list --json ``` **Tool:** `beeps_list_alerts` ```json { "status": "all" } ``` ## Listing Active Alerts ```typescript const activeAlerts = await client.alert.listActive(); console.log(`${activeAlerts.length} active alerts need attention`); ``` ### Safe Method ```typescript const result = await client.alert.listActiveSafe(); if (result.error) { console.error("Failed to list alerts:", result.error.message); } else { console.log(`Active alerts: ${result.data.length}`); } ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/active \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps alert list --active beeps alert list --active --json ``` **Tool:** `beeps_list_alerts` ```json { "status": "active" } ``` Or use the dedicated tool: **Tool:** `beeps_list_active_alerts` ```json {} ``` ## Listing Resolved Alerts ```typescript const resolvedAlerts = await client.alert.listResolved(); console.log(`${resolvedAlerts.length} alerts have been resolved`); ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/resolved \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps alert list --resolved ``` **Tool:** `beeps_list_alerts` ```json { "status": "resolved" } ``` ## Getting a Specific Alert ```typescript const alert = await client.alert.get("alr_abc123"); console.log(`Title: ${alert.title}`); console.log(`Severity: ${alert.severity}`); console.log(`Status: ${alert.resolvedAt ? "Resolved" : "Active"}`); ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/alr_abc123 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps alert get --alert-id alr_abc123 beeps alert get --alert-id alr_abc123 --json ``` **Tool:** `beeps_get_alert` ```json { "alertId": "alr_abc123" } ``` ## Responding to an Alert Signal that you're working on an alert. This creates a responder record. ```typescript const responder = await client.alert.onIt("alr_abc123", { userId: "usr_alice", }); console.log(`${responder.userId} is on it (status: ${responder.status})`); console.log(`Joined at ${responder.joinedAt}`); ``` If `userId` is not provided, the authenticated user is used: ```typescript const responder = await client.alert.onIt("alr_abc123"); ``` ### Safe Method ```typescript const result = await client.alert.onItSafe("alr_abc123", { userId: "usr_alice", }); if (result.error) { console.error("Failed to respond:", result.error.message); } else { console.log(`Responder joined: ${result.data.id}`); } ``` ```bash curl -X POST https://api.beeps.dev/v0/alerts/alr_abc123/on-it \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_alice" }' ``` **Response:** ```json { "responder": { "id": "rsp_def456", "alertId": "alr_abc123", "organizationId": "org_xyz789", "responderType": "user", "userId": "usr_alice", "status": "on_it", "joinedAt": "2025-01-15T10:35:00Z", "completedAt": null } } ``` ```bash beeps alert on-it --alert-id alr_abc123 beeps alert on-it --alert-id alr_abc123 --user-id usr_alice ``` **Tool:** `beeps_on_it` ```json { "alertId": "alr_abc123" } ``` With a specific user: ```json { "alertId": "alr_abc123", "userId": "usr_alice" } ``` ## Listing Responders ```typescript const responders = await client.alert.listResponders("alr_abc123"); responders.forEach((r) => { console.log(`${r.userId || r.integrationId} — ${r.status}`); if (r.prUrl) { console.log(` PR: ${r.prUrl}`); } }); ``` ### Safe Method ```typescript const result = await client.alert.listRespondersSafe("alr_abc123"); if (result.error) { console.error("Failed to list responders:", result.error.message); } else { console.log(`${result.data.length} responders`); } ``` ```bash curl -X GET https://api.beeps.dev/v0/alerts/alr_abc123/responders \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "responders": [ { "id": "rsp_def456", "alertId": "alr_abc123", "organizationId": "org_xyz789", "responderType": "user", "userId": "usr_alice", "status": "on_it", "joinedAt": "2025-01-15T10:35:00Z", "completedAt": null } ] } ``` ```bash beeps alert responders --alert-id alr_abc123 beeps alert responders --alert-id alr_abc123 --json ``` **Tool:** `beeps_list_alert_responders` ```json { "alertId": "alr_abc123" } ``` ## Updating Responder Status Mark a responder as `done` or `dropped`. You can optionally attach a PR URL when completing work. ```typescript const updated = await client.alert.updateResponderStatus( "alr_abc123", "rsp_def456", { status: "done", prUrl: "https://github.com/org/repo/pull/42", }, ); console.log(`Responder status: ${updated.status}`); console.log(`Completed at: ${updated.completedAt}`); ``` ### Safe Method ```typescript const result = await client.alert.updateResponderStatusSafe( "alr_abc123", "rsp_def456", { status: "done" }, ); if (result.error) { console.error("Failed to update:", result.error.message); } else { console.log("Responder marked as done"); } ``` ```bash curl -X PATCH https://api.beeps.dev/v0/alerts/alr_abc123/responders/rsp_def456 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "done", "prUrl": "https://github.com/org/repo/pull/42" }' ``` **Response:** ```json { "responder": { "id": "rsp_def456", "status": "done", "prUrl": "https://github.com/org/repo/pull/42", "completedAt": "2025-01-15T11:00:00Z" } } ``` **Tool:** `beeps_update_responder_status` ```json { "alertId": "alr_abc123", "responderId": "rsp_def456", "status": "done", "prUrl": "https://github.com/org/repo/pull/42" } ``` ## Assigning an Alert ```typescript const assigned = await client.alert.assign("alr_abc123", "usr_bob"); console.log(`Alert assigned to ${assigned.assignedToUserId}`); ``` ### Safe Method ```typescript const result = await client.alert.assignSafe("alr_abc123", "usr_bob"); if (result.error) { console.error("Failed to assign:", result.error.message); } else { console.log(`Assigned to ${result.data.assignedToUserId}`); } ``` ```bash curl -X POST https://api.beeps.dev/v0/alerts/alr_abc123/assign \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "userId": "usr_bob" }' ``` **Response:** ```json { "alert": { "id": "alr_abc123", "assignedToUserId": "usr_bob", "updatedAt": "2025-01-15T10:40:00Z" } } ``` ```bash beeps alert assign --alert-id alr_abc123 --user-id usr_bob ``` **Tool:** `beeps_assign_alert` ```json { "alertId": "alr_abc123", "userId": "usr_bob" } ``` ## Resolving an Alert ```typescript const resolved = await client.alert.resolve("alr_abc123"); console.log(`Alert resolved at ${resolved.resolvedAt}`); ``` ### Safe Method ```typescript const result = await client.alert.resolveSafe("alr_abc123"); if (result.error) { console.error("Failed to resolve:", result.error.message); } else { console.log("Alert resolved"); } ``` ```bash curl -X POST https://api.beeps.dev/v0/alerts/alr_abc123/resolve \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "alert": { "id": "alr_abc123", "resolvedAt": "2025-01-15T10:45:00Z", "updatedAt": "2025-01-15T10:45:00Z" } } ``` ```bash beeps alert resolve --alert-id alr_abc123 ``` **Tool:** `beeps_resolve_alert` ```json { "alertId": "alr_abc123" } ``` ## Type Definitions ### Alert ```typescript type Alert = { id: string; organizationId: string; webhookId: string; title: string; message: string | null; severity: AlertSeverity; source: string; externalId: string | null; metadata: Record | null; assignedToUserId: string | null; resolvedAt: string | null; resolvedBy: ResolutionSource | null; resolvedByUserId: string | null; resolvedByProvider: string | null; createdAt: string; updatedAt: string; }; ``` ### AlertSeverity ```typescript type AlertSeverity = "critical" | "high" | "medium" | "low" | "info"; ``` ### ResolutionSource ```typescript type ResolutionSource = "user" | "monitoring_system"; ``` ### AlertResponder ```typescript type AlertResponder = { id: string; alertId: string; organizationId: string; responderType: ResponderType; userId: string | null; integrationId: string | null; agentSessionId: string | null; status: ResponderStatus; prUrl: string | null; joinedAt: string; completedAt: string | null; }; ``` ### ResponderType ```typescript type ResponderType = "user" | "agent"; ``` ### ResponderStatus ```typescript type ResponderStatus = "on_it" | "done" | "dropped"; ``` ### OnItInput ```typescript type OnItInput = { userId?: string; }; ``` ### UpdateResponderStatusInput ```typescript type UpdateResponderStatusInput = { status: "done" | "dropped"; prUrl?: string; }; ``` ## Common Patterns ### Complete Alert Workflow ```typescript const alertId = "alr_abc123"; const userId = "usr_alice"; const alert = await client.alert.get(alertId); console.log(`New alert: ${alert.title}`); const responder = await client.alert.onIt(alertId, { userId }); console.log(`On it: ${responder.status}`); await client.alert.assign(alertId, userId); console.log("Assigned to self"); console.log("Working on the issue..."); await client.alert.updateResponderStatus(alertId, responder.id, { status: "done", prUrl: "https://github.com/org/repo/pull/42", }); console.log("Responder done"); await client.alert.resolve(alertId); console.log("Resolved"); ``` ### Filter Alerts by Severity ```typescript const active = await client.alert.listActive(); const critical = active.filter((a) => a.severity === "critical"); const high = active.filter((a) => a.severity === "high"); console.log(`Critical: ${critical.length}`); console.log(`High: ${high.length}`); ``` ### Alert Dashboard ```typescript const active = await client.alert.listActive(); const stats = { total: active.length, assigned: active.filter((a) => a.assignedToUserId).length, critical: active.filter((a) => a.severity === "critical").length, }; console.log("Alert Dashboard:"); console.log(`Total Active: ${stats.total}`); console.log(`Assigned: ${stats.assigned}`); console.log(`Critical: ${stats.critical}`); ``` ### My Assigned Alerts ```typescript const userId = "usr_alice"; const active = await client.alert.listActive(); const myAlerts = active.filter((a) => a.assignedToUserId === userId); console.log(`You have ${myAlerts.length} assigned alerts:`); myAlerts.forEach((alert) => { console.log(`- [${alert.severity}] ${alert.title}`); }); ``` ## Troubleshooting **Alerts not appearing?** See [Troubleshooting - Alerts Not Appearing](/docs/troubleshooting/#alerts-are-not-appearing). --- # Alert API Reference > Source: https://beeps.dev/docs/alerts/api/ Complete API reference for the Alert resource in the beeps SDK. ## Alert Methods ### `client.alert.list()` List all alerts in your organization. **Returns:** `Promise` **Example:** ```typescript const alerts = await client.alert.list(); ``` ### `client.alert.listActive()` List all active (unresolved) alerts. **Returns:** `Promise` **Example:** ```typescript const activeAlerts = await client.alert.listActive(); ``` ### `client.alert.listResolved()` List all resolved alerts. **Returns:** `Promise` **Example:** ```typescript const resolvedAlerts = await client.alert.listResolved(); ``` ### `client.alert.get(alertId)` Get a specific alert by ID. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const alert = await client.alert.get("alr_abc123"); ``` ### `client.alert.onIt(alertId, input?)` Signal that a user is responding to an alert. Creates a responder record. If `userId` is not provided, the authenticated user is used. **Parameters:** - `alertId: string` - `input?: OnItInput` **Returns:** `Promise` **Example:** ```typescript const responder = await client.alert.onIt("alr_abc123", { userId: "usr_alice", }); // Or as the authenticated user const responder = await client.alert.onIt("alr_abc123"); ``` ### `client.alert.listResponders(alertId)` List all responders for an alert. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const responders = await client.alert.listResponders("alr_abc123"); ``` ### `client.alert.listAgents(alertId)` List agent jobs associated with an alert. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const jobs = await client.alert.listAgents("alr_abc123"); ``` ### `client.alert.getFixContext(alertId)` Get a foreground fix context bundle for an alert, including responders, agent jobs, and suggested next actions. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const fixContext = await client.alert.getFixContext("alr_abc123"); console.log(fixContext.nextActions); ``` ### `client.alert.updateResponderStatus(alertId, responderId, input)` Update a responder's status to `done` or `dropped`. Optionally attach a PR URL. **Parameters:** - `alertId: string` - `responderId: string` - `input: UpdateResponderStatusInput` **Returns:** `Promise` **Example:** ```typescript const updated = await client.alert.updateResponderStatus( "alr_abc123", "rsp_def456", { status: "done", prUrl: "https://github.com/org/repo/pull/42" }, ); ``` ### `client.alert.resolve(alertId)` Mark an alert as resolved. **Parameters:** - `alertId: string` **Returns:** `Promise` **Example:** ```typescript const alert = await client.alert.resolve("alr_abc123"); ``` ### `client.alert.assign(alertId, userId)` Assign an alert to a user. **Parameters:** - `alertId: string` - `userId: string` **Returns:** `Promise` **Example:** ```typescript const alert = await client.alert.assign("alr_abc123", "usr_bob"); ``` ## Safe Methods All methods have corresponding `*Safe` variants that return `Result` instead of throwing: - `client.alert.listSafe()` - `client.alert.listActiveSafe()` - `client.alert.listResolvedSafe()` - `client.alert.getSafe(alertId)` - `client.alert.onItSafe(alertId, input?)` - `client.alert.listRespondersSafe(alertId)` - `client.alert.listAgentsSafe(alertId)` - `client.alert.getFixContextSafe(alertId)` - `client.alert.updateResponderStatusSafe(alertId, responderId, input)` - `client.alert.resolveSafe(alertId)` - `client.alert.assignSafe(alertId, userId)` **Example:** ```typescript const result = await client.alert.onItSafe("alr_abc123", { userId: "usr_alice", }); if (result.error) { console.error(result.error.message); } else { console.log(`Responder joined: ${result.data.id}`); } ``` ## Types ### AlertSeverity ```typescript type AlertSeverity = "critical" | "high" | "medium" | "low" | "info"; ``` ### Alert ```typescript type Alert = { id: string; organizationId: string; webhookId: string; title: string; message: string | null; severity: AlertSeverity; source: string; externalId: string | null; metadata: Record | null; assignedToUserId: string | null; resolvedAt: string | null; resolvedBy: ResolutionSource | null; resolvedByUserId: string | null; resolvedByProvider: string | null; createdAt: string; updatedAt: string; }; ``` ### ResolutionSource ```typescript type ResolutionSource = "user" | "monitoring_system"; ``` ### AlertResponder ```typescript type AlertResponder = { id: string; alertId: string; organizationId: string; responderType: ResponderType; userId: string | null; integrationId: string | null; agentSessionId: string | null; status: ResponderStatus; prUrl: string | null; joinedAt: string; completedAt: string | null; }; ``` ### ResponderType ```typescript type ResponderType = "user" | "agent"; ``` ### ResponderStatus ```typescript type ResponderStatus = "on_it" | "done" | "dropped"; ``` ### OnItInput ```typescript type OnItInput = { userId?: string; }; ``` ### UpdateResponderStatusInput ```typescript type UpdateResponderStatusInput = { status: "done" | "dropped"; prUrl?: string; }; ``` ## HTTP Endpoints ### List All Alerts ``` GET /v0/alerts ``` ### List Active Alerts ``` GET /v0/alerts/active ``` ### List Resolved Alerts ``` GET /v0/alerts/resolved ``` ### Get Alert ``` GET /v0/alerts/:alertId ``` ### Respond to Alert (On It) ``` POST /v0/alerts/:alertId/on-it ``` **Request Body:** ```json { "userId": "usr_alice" } ``` ### List Responders ``` GET /v0/alerts/:alertId/responders ``` ### List Agent Jobs for Alert ``` GET /v0/alerts/:alertId/agents ``` ### Get Fix Context ``` GET /v0/alerts/:alertId/fix-context ``` ### Update Responder Status ``` PATCH /v0/alerts/:alertId/responders/:responderId ``` **Request Body:** ```json { "status": "done", "prUrl": "https://github.com/org/repo/pull/42" } ``` ### Resolve Alert ``` POST /v0/alerts/:alertId/resolve ``` ### Assign Alert ``` POST /v0/alerts/:alertId/assign ``` **Request Body:** ```json { "userId": "usr_bob" } ``` ## Authentication All API requests require authentication using your API key: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.beeps.dev/v0/alerts ``` --- # Alert Storms Overview > Source: https://beeps.dev/docs/alert-storms/overview/ An alert storm happens when a single root cause (e.g., a database going down) triggers many different alerts — API timeouts, connection refused errors, query failures, and so on. Without storm detection, each unique alert title would dispatch its own agent, leading to multiple agents fixing symptoms instead of the root cause. ## How Storm Detection Works Storm detection is purely count-based. No AI is involved in the detection step — it simply counts alerts per relay within a time window. ### Normal Flow (No Storm) When alerts arrive at a normal rate: 1. Alert arrives via webhook 2. Relay rules execute 3. Agent rule dispatches a coding agent 4. Agent investigates and creates a PR ### Storm Flow When a burst of alerts arrives: 1. First 2 alerts dispatch agents immediately (configurable via `maxImmediateDispatches`) 2. Once the alert count exceeds the threshold within the time window, a storm is detected 3. Subsequent alerts are held — no additional agents are dispatched 4. After a debounced delay, an AI triage step analyzes all storm alerts 5. The triage identifies the root cause alert 6. A single agent is dispatched for the root cause with full storm context ## Storm Lifecycle | Status | Description | |--------|-------------| | `collecting` | Storm detected, alerts are being collected | | `triaging` | AI is analyzing the alerts to find the root cause | | `dispatched` | Root cause identified, agent dispatched | | `resolved` | Storm resolved (future enhancement) | ## Relationship to Alert Deduplication Storm detection is a layer above the existing same-title dedup: - **Same-title dedup** prevents duplicate agents for alerts with identical (normalized) titles - **Storm detection** prevents excessive agents when many _different_ alert titles fire from a shared root cause Both checks run in sequence. Storm detection runs first — if it holds an alert, the same-title dedup is skipped entirely. ## Configuration Storm behavior is configured per agent rule. See [Tuning Storm Detection](/docs/alert-storms/tuning/) for configuration options. ```typescript const rule = { ruleType: "agent", config: { agentType: "devin", integrationId: "int_abc123", stormThreshold: 5, stormWindowSeconds: 60, maxImmediateDispatches: 2, }, }; ``` ## Fail-Open Design Storm detection is designed to never block alerts: - If the storm detection database query fails, the alert proceeds to normal dispatch - If the AI triage fails, the earliest high-severity alert is selected as root cause - Existing alert routing, notifications, and acknowledgment are unaffected by storm detection --- # Tuning Storm Detection > Source: https://beeps.dev/docs/alert-storms/tuning/ Storm detection is configured per agent rule, so each relay can have independent thresholds tuned to its alert volume and criticality. ## Configuration Options ### `stormThreshold` The number of alerts within the time window that triggers a storm. - **Type:** `number` - **Default:** `5` - **Range:** `2` to `50` ```typescript { stormThreshold: 5 } ``` ### `stormWindowSeconds` The time window (in seconds) used to count alerts for storm detection. - **Type:** `number` - **Default:** `60` - **Range:** `10` to `300` ```typescript { stormWindowSeconds: 60 } ``` ### `maxImmediateDispatches` The number of agents dispatched immediately before storm hold kicks in. These first agents start working right away while the storm is still being evaluated. - **Type:** `number` - **Default:** `2` - **Range:** `0` to `5` ```typescript { maxImmediateDispatches: 2 } ``` Set to `0` to hold all dispatches during a storm. Set to a higher number if you want more agents working in parallel before triage. ## Examples ### High-Traffic Relay For a relay that receives many alerts normally, increase the threshold to avoid false storm detection: ```typescript { agentType: "devin", integrationId: "int_abc123", stormThreshold: 15, stormWindowSeconds: 120, maxImmediateDispatches: 3, } ``` ### Critical Relay For a relay where every alert matters and storms should be detected quickly: ```typescript { agentType: "cursor", integrationId: "int_def456", repository: "https://github.com/org/critical-service", stormThreshold: 3, stormWindowSeconds: 30, maxImmediateDispatches: 1, } ``` ## Debounced Triage Window When a storm is detected, the triage job doesn't fire immediately. Instead, it uses a debounced delay to wait for more alerts to arrive: - Each new alert pushes the triage job forward by **15 seconds** - The maximum delay is capped at **90 seconds** from when the storm was first detected - This ensures late-arriving alerts (which may include the actual root cause) are included in triage For example, if a storm is detected at `T=0`: - Alert at `T=5s` pushes triage to `T=20s` - Alert at `T=10s` pushes triage to `T=25s` - Alert at `T=20s` pushes triage to `T=35s` - No more alerts arrive — triage fires at `T=35s` If alerts keep arriving, triage fires at most at `T=90s` regardless. ## AI Triage After the debounce window, AI analyzes all collected storm alerts. The triage considers: 1. **Timing** — earlier alerts are more likely to be the root cause 2. **Infrastructure level** — lower-level alerts (database, network) rank higher than application-level alerts 3. **Severity** — higher severity alerts with specific error details are more informative 4. **Error specificity** — alerts with stack traces, connection errors, or specific error codes are preferred 5. **Causal relationships** — patterns where one failure causes others (e.g., DB down causing API timeouts) The identified root cause alert is then dispatched to a coding agent with the full storm context, so the agent understands it needs to fix the underlying issue rather than a symptom. --- # Integrations Overview > Source: https://beeps.dev/docs/observability-integrations/overview/ Point your monitoring tools at a relay webhook URL and beeps handles the rest. ## Getting your webhook URL Each relay has a webhook URL in the format `https://hooks.beeps.dev/YOUR_WEBHOOK_ID`. Grab it from your relay in the dashboard and paste it into your monitoring provider's webhook settings. ## Supported providers - [Sentry](/docs/observability-integrations/sentry/) - [Datadog](/docs/observability-integrations/datadog/) - [Axiom](/docs/observability-integrations/axiom/) - [Custom webhooks](/docs/observability-integrations/custom/) — anything that can POST JSON ## How it works 1. Add your relay webhook URL to your monitoring provider 2. Configure alert rules in the provider (what triggers a webhook) 3. When a webhook fires, beeps creates an alert and runs your relay rules ## Testing Fire a test alert from your monitoring tool and check the dashboard. If nothing shows up, see [Troubleshooting](/docs/troubleshooting/#alerts-are-not-appearing). --- # Sentry > Source: https://beeps.dev/docs/observability-integrations/sentry/ Configure Sentry to send error alerts to beeps via webhooks. ## Prerequisites - Active Sentry account and project - beeps relay webhook URL ## Setup Steps 1. In Sentry, navigate to **Settings** → **Integrations** 2. Search for and select **Webhooks** 3. Click **Add to Project** or **Configure** 4. Enter your beeps webhook URL: `https://hooks.beeps.dev/YOUR_WEBHOOK_ID` 5. Select the events you want to trigger alerts: - Error events - Issue state changes - New releases 6. Save the webhook configuration ## Alert Rules Create alert rules to control when notifications are sent: 1. Go to **Alerts** → **Create Alert** 2. Choose **Issues** or **Errors** 3. Set your conditions (e.g., "When an event is seen more than 100 times in 1 hour") 4. Under **Actions**, select **Send a notification via an integration** 5. Choose your beeps webhook 6. Save the rule ## What happens next Once Sentry fires the webhook, beeps creates an alert and runs it through your relay's rules. What happens from there depends on how you've set up your [relay rules](/docs/relays/rules/) — notify on-call, invoke an agent, post to Slack, or any combination. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Datadog > Source: https://beeps.dev/docs/observability-integrations/datadog/ Configure Datadog monitors to send alerts to beeps via webhooks. ## Prerequisites - Active Datadog account - beeps relay webhook URL ## Setup Steps ### Create Webhook Integration 1. In Datadog, navigate to **Integrations** → **Webhooks** 2. Click **New** to create a webhook 3. Name your webhook (e.g., "beeps Production") 4. Enter URL: `https://hooks.beeps.dev/YOUR_WEBHOOK_ID` 5. Optionally customize the payload template (see below) 6. Save the webhook ### Configure Monitors Add the webhook to your monitors: 1. Create or edit a monitor 2. In the **Notify your team** section, add `@webhook-beeps-dev-production` (using your webhook name) 3. Customize the message with monitor details 4. Save the monitor ## Alert Renotification Configure monitors to re-notify if not resolved: 1. In monitor settings, enable **Renotify if this monitor stays in alert state** 2. Set interval (e.g., every 60 minutes) 3. Include in message: `This is a reminder that the alert is still active` ## What happens next When a Datadog monitor triggers, beeps normalizes the payload into an alert and runs your relay rules. Renotification webhooks from Datadog create follow-up alerts if the original is still active. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Axiom > Source: https://beeps.dev/docs/observability-integrations/axiom/ Configure Axiom monitors to send alerts to beeps via webhooks. ## Prerequisites - Active Axiom account - beeps relay webhook URL ## Setup Steps 1. In Axiom, navigate to **Monitors** 2. Create a new monitor or edit an existing one 3. Configure your query and alert conditions 4. In **Notifications**, select **Webhook** 5. Enter URL: `https://hooks.beeps.dev/YOUR_WEBHOOK_ID` 6. Choose notification frequency and threshold 7. Save the monitor ## Webhook Payload Axiom sends webhooks with this structure: ```json { "monitor": { "id": "mon_abc123", "name": "High 5xx Error Rate", "description": "Alert when 5xx errors exceed threshold", "query": "['http-logs'] | where status >= 500" }, "alert": { "state": "triggered", "value": 25, "threshold": 10, "operator": ">" }, "timestamp": "2024-10-27T10:30:00Z", "queryUrl": "https://app.axiom.co/...", "dataset": "http-logs" } ``` ## Notification Settings ### Frequency Options - **Every time**: Alert on every evaluation that meets threshold - **Once**: Alert only once when condition is first met - **Every X minutes**: Rate limit notifications ### Alert Resolution Configure when to send "resolved" notifications: ```typescript { "sendResolved": true, "resolutionThreshold": { "operator": "<=", "value": 5 } } ``` ## What happens next When an Axiom monitor fires, beeps creates an alert from the payload and runs your relay rules. If you've configured `sendResolved: true`, Axiom will also send a resolution webhook that beeps can use to auto-resolve the alert. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Custom Webhooks > Source: https://beeps.dev/docs/observability-integrations/custom/ Send alerts to beeps from any system using HTTP POST requests. ## Webhook Format beeps relays accept webhook POST requests at: ``` https://hooks.beeps.dev/YOUR_WEBHOOK_ID ``` ## Basic Example Send a simple alert: ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_ID \ -H "Content-Type: application/json" \ -d '{ "title": "Database Connection Failed", "message": "Unable to connect to production database", "severity": "critical" }' ``` ## Payload Structure ### Required Fields - `title` (string): Short alert title (max 255 characters) ### Optional Fields - `message` (string): Detailed alert description - `severity` (string): Alert severity level - `low` / `info` - `medium` / `warning` - `high` / `error` - `critical` / `fatal` - `source` (string): System generating the alert - `timestamp` (string): ISO 8601 timestamp (defaults to current time) - `metadata` (object): Additional key-value pairs ### Complete Example ```json { "title": "High API Error Rate", "message": "Production API error rate exceeded 5% threshold. Current rate: 8.2%", "severity": "critical", "source": "monitoring-system", "timestamp": "2024-10-27T10:30:00Z", "metadata": { "environment": "production", "service": "api", "region": "us-east-1", "error_rate": 8.2, "threshold": 5.0, "dashboard_url": "https://dashboard.example.com/api", "runbook_url": "https://wiki.example.com/runbooks/high-error-rate" } } ``` ## Language Examples ### Python ```python import requests from datetime import datetime def send_alert(title, message, severity="high"): webhook_url = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID" payload = { "title": title, "message": message, "severity": severity, "source": "python-app", "timestamp": datetime.utcnow().isoformat() + "Z", "metadata": { "host": "app-server-01", "pid": 12345 } } response = requests.post(webhook_url, json=payload) response.raise_for_status() return response.json() send_alert( "Database Connection Failed", "Could not connect to PostgreSQL after 3 retries", severity="critical" ) ``` ### Node.js ```javascript async function sendAlert(title, message, severity = "high") { const webhookUrl = "https://hooks.beeps.dev/YOUR_WEBHOOK_ID"; const payload = { title, message, severity, source: "nodejs-app", timestamp: new Date().toISOString(), metadata: { host: process.env.HOSTNAME, pid: process.pid, }, }; const response = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { throw new Error(`Failed to send alert: ${response.statusText}`); } return response.json(); } await sendAlert( "High Memory Usage", "Application memory usage exceeded 90%", "warning", ); ``` ### Go ```go package main import ( "bytes" "encoding/json" "net/http" "time" ) type Alert struct { Title string `json:"title"` Message string `json:"message"` Severity string `json:"severity"` Source string `json:"source"` Timestamp string `json:"timestamp"` Metadata map[string]interface{} `json:"metadata"` } func sendAlert(title, message, severity string) error { webhookURL := "https://hooks.beeps.dev/YOUR_WEBHOOK_ID" alert := Alert{ Title: title, Message: message, Severity: severity, Source: "go-app", Timestamp: time.Now().UTC().Format(time.RFC3339), Metadata: map[string]interface{}{ "host": "app-server-01", "version": "1.2.3", }, } payload, err := json.Marshal(alert) if err != nil { return err } resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(payload)) if err != nil { return err } defer resp.Body.Close() return nil } func main() { sendAlert( "Service Unavailable", "Health check failed for payment service", "critical", ) } ``` ### Ruby ```ruby require 'net/http' require 'json' require 'uri' def send_alert(title, message, severity: 'high') webhook_url = URI('https://hooks.beeps.dev/YOUR_WEBHOOK_ID') payload = { title: title, message: message, severity: severity, source: 'ruby-app', timestamp: Time.now.utc.iso8601, metadata: { host: `hostname`.strip, ruby_version: RUBY_VERSION } } http = Net::HTTP.new(webhook_url.host, webhook_url.port) http.use_ssl = true request = Net::HTTP::Post.new(webhook_url) request['Content-Type'] = 'application/json' request.body = payload.to_json response = http.request(request) raise "Failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) end send_alert( 'Cache Miss Rate High', 'Redis cache miss rate exceeded 50%', severity: 'warning' ) ``` ### Bash ```bash #!/bin/bash WEBHOOK_URL="https://hooks.beeps.dev/YOUR_WEBHOOK_ID" send_alert() { local title="$1" local message="$2" local severity="${3:-high}" curl -X POST "$WEBHOOK_URL" \ -H "Content-Type: application/json" \ -d @- < { return new Promise((resolve, reject) => { const options = { host: domain, port: 443, method: "GET", rejectUnauthorized: false, agent: false, }; const req = https.request(options, (res) => { const cert = res.socket.getPeerCertificate(); if (!cert || Object.keys(cert).length === 0) { reject(new Error("No certificate found")); return; } const validTo = new Date(cert.valid_to); const now = new Date(); const daysRemaining = Math.floor( (validTo.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) ); resolve({ domain, validTo, daysRemaining, issuer: cert.issuer.O || "Unknown", subject: cert.subject.CN || domain, }); }); req.on("error", reject); req.end(); }); } async function sendAlert(cert: CertificateInfo) { const severity = cert.daysRemaining <= 7 ? "critical" : "warning"; const payload = { title: `SSL Certificate Expiring Soon: ${cert.domain}`, message: `SSL certificate for ${cert.domain} expires in ${cert.daysRemaining} days (${cert.validTo.toISOString()})`, severity, source: "ssl-monitor", timestamp: new Date().toISOString(), metadata: { domain: cert.domain, days_remaining: cert.daysRemaining, expiration_date: cert.validTo.toISOString(), issuer: cert.issuer, subject: cert.subject, }, }; try { const response = await fetch(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) { console.error(`Failed to send alert: ${response.statusText}`); } else { console.log(`Alert sent for ${cert.domain}`); } } catch (error) { console.error(`Error sending alert:`, error); } } async function checkAllCertificates() { console.log(`Checking ${DOMAINS.length} certificates...`); for (const domain of DOMAINS) { try { const cert = await checkCertificate(domain); console.log( `${domain}: ${cert.daysRemaining} days remaining (expires ${cert.validTo.toLocaleDateString()})` ); if (cert.daysRemaining <= WARNING_DAYS) { await sendAlert(cert); } } catch (error) { console.error(`Error checking ${domain}:`, error); await fetch(WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: `SSL Certificate Check Failed: ${domain}`, message: `Unable to retrieve certificate: ${error.message}`, severity: "high", source: "ssl-monitor", timestamp: new Date().toISOString(), metadata: { domain, error: error.message, }, }), }); } } } checkAllCertificates(); setInterval(checkAllCertificates, 24 * 60 * 60 * 1000); ``` **Usage:** ```bash # Install dependencies npm install # Run the monitor npx tsx ssl-monitor.ts # Or run as a cron job (daily at 9 AM) 0 9 * * * cd /path/to/monitor && npx tsx ssl-monitor.ts ``` ## Response Format Successful webhook submission returns: ```json { "success": true, "alert_id": "alr_abc123", "received_at": "2024-10-27T10:30:00Z" } ``` ## Error Handling ### HTTP Status Codes - `200 OK`: Alert received successfully - `400 Bad Request`: Invalid payload format - `401 Unauthorized`: Invalid webhook ID - `429 Too Many Requests`: Rate limit exceeded - `500 Internal Server Error`: Server error ### Example Error Response ```json { "success": false, "error": "Invalid payload: 'title' is required", "code": "INVALID_PAYLOAD" } ``` ## Best Practices 1. **Include Context**: Add relevant metadata for triage 2. **Use Severity**: Properly categorize alert urgency 3. **Add Timestamps**: Include when issue occurred 4. **Link to Resources**: Add dashboard/runbook URLs 5. **Handle Errors**: Implement retry logic with backoff 6. **Rate Limit**: Batch alerts when possible 7. **Validate Payloads**: Check data before sending 8. **Use HTTPS**: Always use secure connections ## Rate Limits - **Default**: 100 requests per minute per webhook - **Burst**: Up to 10 requests per second - Exceeding limits returns `429 Too Many Requests` ## What happens next Once beeps receives a POST, it creates an alert and runs your [relay rules](/docs/relays/rules/). The response includes the `alert_id` so you can track it programmatically. ## Next Steps - [Configure Relay Rules](/docs/relays/rules/) to route alerts to humans and AI agents - [Create Schedules](/docs/schedules/overview/) to define on-call rotations - [Add Contact Methods](/docs/contact-methods/overview/) for email and SMS notifications --- # Agent Integrations Overview > Source: https://beeps.dev/docs/agent-integrations/overview/ Agent integrations store credentials for AI agents (Devin, Cursor) that relay rules can invoke. API keys are encrypted and never returned in API responses. ## Supported Providers ### Devin AI agent for autonomous software engineering tasks. Get your API key (`apk_*`) from the [Devin API settings](https://app.devin.ai/settings/api-keys). ```typescript const devinIntegration = await client.integration.create({ name: "Devin Production", provider: "devin", apiKey: "devin_api_key_here", }); ``` ### Cursor AI coding agent that creates PRs with fixes. Requires a repository URL in the agent rule config. Get your API key from the [Cursor dashboard](https://cursor.com/dashboard?tab=cloud-agents). **Prerequisites:** Privacy Mode must be enabled in your Cursor account settings for API-based agent access to work. Without it, all API requests will return `403 Forbidden`. ```typescript const cursorIntegration = await client.integration.create({ name: "Cursor Agent", provider: "cursor", apiKey: "cursor_api_key_here", }); ``` ## Using Integrations with Relay Rules Reference integrations in relay rules by ID: ```typescript const integration = await client.integration.create({ name: "Devin Production", provider: "devin", apiKey: process.env.DEVIN_API_KEY, }); await client.relay.rules.create(relayId, { name: "Try AI agent first", ruleType: "agent", config: { agentType: "devin", integrationId: integration.id, pollInterval: 30000, maxPollAttempts: 120, }, }); ``` ### Cursor Agent Example Cursor requires a `repository` URL in the config: ```typescript const cursorIntegration = await client.integration.create({ name: "Cursor Agent", provider: "cursor", apiKey: process.env.CURSOR_API_KEY, }); await client.relay.rules.create(relayId, { name: "Cursor Auto-Fix", ruleType: "agent", config: { agentType: "cursor", integrationId: cursorIntegration.id, repository: "https://github.com/org/repo", autoCreatePr: true, pollInterval: 30000, maxPollAttempts: 120, }, }); ``` ## Best Practices See [Best Practices - Agent Integrations](/docs/best-practices/#agent-integrations) for configuration guidelines. ## MCP Clients To connect beeps tools in Claude Code or Codex, see [MCP Setup for Claude Code and Codex](/docs/using-beeps/mcp-server/). --- # Managing Integrations > Source: https://beeps.dev/docs/agent-integrations/manage/ import { Tabs, TabItem } from '@astrojs/starlight/components'; Create, update, and manage integrations for AI agents. ## Creating an Integration ```typescript import { BeepsClient } from "@beepsdev/sdk"; const client = new BeepsClient({ apiKey: process.env.BEEPS_API_KEY, }); const integration = await client.integration.create({ name: "Devin Production", provider: "devin", apiKey: process.env.DEVIN_API_KEY, metadata: { environment: "production", endpoint: "https://api.devin.ai/v3/organizations/sessions", }, }); console.log(`Created integration: ${integration.id}`); ``` ### Devin Integration ```typescript const devin = await client.integration.create({ name: "Devin Agent", provider: "devin", apiKey: "devin_api_key_here", metadata: { workspace: "production", defaultPriority: "high", }, }); ``` ### Cursor Integration ```typescript const cursor = await client.integration.create({ name: "Cursor Agent", provider: "cursor", apiKey: "cursor_api_key_here", }); ``` ### Safe Method (No Exceptions) ```typescript const result = await client.integration.createSafe({ name: "Devin Production", provider: "devin", apiKey: process.env.DEVIN_API_KEY, }); if (result.error) { console.error("Failed to create integration:", result.error.message); } else { console.log(`Created: ${result.data.id}`); } ``` ```bash curl -X POST https://api.beeps.dev/integrations \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Devin Production", "provider": "devin", "apiKey": "devin_api_key_here", "metadata": { "environment": "production" } }' ``` **Response:** ```json { "integration": { "id": "int_abc123", "organizationId": "org_xyz789", "name": "Devin Production", "provider": "devin", "apiKey": "***", "metadata": { "environment": "production" }, "createdBy": "usr_alice", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } } ``` ```bash beeps integration create \ --provider devin \ --name "Devin Production" \ --api-key-env DEVIN_API_KEY ``` Add `--json` for machine-readable output: ```bash beeps integration create \ --provider devin \ --name "Devin Production" \ --api-key-env DEVIN_API_KEY \ --json ``` ## Listing Integrations ```typescript const integrations = await client.integration.list(); console.log(`Found ${integrations.length} integrations:`); integrations.forEach((integration) => { console.log(`- ${integration.name} (${integration.provider})`); }); ``` ### Filter by Provider ```typescript const integrations = await client.integration.list(); const devinIntegrations = integrations.filter( (i) => i.provider === "devin" ); const cursorIntegrations = integrations.filter( (i) => i.provider === "cursor" ); console.log(`Devin: ${devinIntegrations.length}`); console.log(`Cursor: ${cursorIntegrations.length}`); ``` ```bash curl -X GET https://api.beeps.dev/integrations \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "integrations": [ { "id": "int_abc123", "organizationId": "org_xyz789", "name": "Devin Production", "provider": "devin", "apiKey": "***", "metadata": { "environment": "production" }, "createdBy": "usr_alice", "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z", "deletedAt": null } ] } ``` ```bash beeps integration list ``` Add `--json` for machine-readable output: ```bash beeps integration list --json ``` **Tool:** `beeps_list_integrations` ```json {} ``` ## Getting a Specific Integration ```typescript const integration = await client.integration.get("int_abc123"); console.log(`Name: ${integration.name}`); console.log(`Provider: ${integration.provider}`); console.log(`Metadata:`, integration.metadata); ``` ```bash curl -X GET https://api.beeps.dev/integrations/int_abc123 \ -H "Authorization: Bearer YOUR_API_KEY" ``` ```bash beeps integration get --integration-id int_abc123 ``` Add `--json` for machine-readable output: ```bash beeps integration get --integration-id int_abc123 --json ``` ## Updating an Integration ```typescript const updated = await client.integration.update("int_abc123", { name: "Devin Production (Updated)", apiKey: process.env.NEW_DEVIN_API_KEY, metadata: { environment: "production", version: "v2", }, }); console.log("Integration updated"); ``` ### Update Name Only ```typescript const updated = await client.integration.update("int_abc123", { name: "New Integration Name", }); ``` ### Rotate API Key ```typescript const updated = await client.integration.update("int_abc123", { apiKey: process.env.NEW_API_KEY, }); console.log("API key rotated"); ``` ### Update Metadata ```typescript const updated = await client.integration.update("int_abc123", { metadata: { endpoint: "https://new-endpoint.example.com", timeout: 60000, }, }); ``` ```bash curl -X PUT https://api.beeps.dev/integrations/int_abc123 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Devin Production (Updated)", "apiKey": "new_api_key_here" }' ``` ```bash beeps integration update \ --integration-id int_abc123 \ --name "Devin Production (Updated)" \ --api-key-env NEW_DEVIN_API_KEY ``` Add `--json` for machine-readable output: ```bash beeps integration update \ --integration-id int_abc123 \ --name "Devin Production (Updated)" \ --api-key-env NEW_DEVIN_API_KEY \ --json ``` ## Deleting an Integration ```typescript await client.integration.delete("int_abc123"); console.log("Integration deleted successfully"); ``` ### Safe Method ```typescript const result = await client.integration.deleteSafe("int_abc123"); if (result.error) { console.error("Failed to delete:", result.error.message); } else { console.log("Deleted:", result.data.success); } ``` ```bash curl -X DELETE https://api.beeps.dev/integrations/int_abc123 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "success": true } ``` ```bash beeps integration delete --integration-id int_abc123 ``` Add `--json` for machine-readable output: ```bash beeps integration delete --integration-id int_abc123 --json ``` ## Type Definitions ### IntegrationProvider ```typescript type IntegrationProvider = "devin" | "cursor"; ``` ### CreateIntegrationInput ```typescript type CreateIntegrationInput = { name: string; // Integration name provider: IntegrationProvider; // Service provider apiKey: string; // API key (stored securely) metadata?: Record; // Provider-specific config }; ``` ### UpdateIntegrationInput ```typescript type UpdateIntegrationInput = { name?: string; // Update name apiKey?: string; // Rotate API key metadata?: Record; // Update metadata }; ``` ### Integration ```typescript type Integration = { id: string; organizationId: string; name: string; provider: IntegrationProvider; apiKey: string; // Always returns "***" for security metadata: Record | null; createdBy: string; // User who created it createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ## Common Patterns ### Complete Setup with Relay Rule ```typescript const integration = await client.integration.create({ name: "Devin Auto-Triage", provider: "devin", apiKey: process.env.DEVIN_API_KEY, }); const relay = await client.relay.create({ name: "Production Alerts", }); await client.relay.rules.create(relay.id, { name: "AI triage first", ruleType: "agent", order: 1, config: { agentType: "devin", integrationId: integration.id, pollInterval: 30000, }, }); await client.relay.rules.create(relay.id, { name: "Escalate to humans", ruleType: "schedule_notify", order: 2, config: { scheduleId: "sch_oncall", }, }); ``` ### Key Rotation ```typescript const integrations = await client.integration.list(); for (const integration of integrations) { if (integration.provider === "devin") { await client.integration.update(integration.id, { apiKey: process.env.NEW_DEVIN_KEY, }); console.log(`Rotated key for ${integration.name}`); } } ``` ### Environment-Specific Integrations ```typescript const environments = ["development", "staging", "production"]; for (const env of environments) { await client.integration.create({ name: `Devin - ${env}`, provider: "devin", apiKey: process.env[`DEVIN_${env.toUpperCase()}_KEY`], metadata: { environment: env, }, }); } ``` ## Troubleshooting **AI agent not responding?** See [Troubleshooting - AI Agent Never Responds](/docs/troubleshooting/#ai-agent-never-responds). --- # Integration API Reference > Source: https://beeps.dev/docs/agent-integrations/api/ Complete API reference for the Integration resource in the beeps SDK. ## Integration Methods ### `client.integration.create(input)` Create a new integration. **Parameters:** - `input: CreateIntegrationInput` **Returns:** `Promise` **Example:** ```typescript const integration = await client.integration.create({ name: "Devin Production", provider: "devin", apiKey: process.env.DEVIN_API_KEY, metadata: { environment: "production", }, }); ``` ### `client.integration.list()` List all integrations in your organization. **Returns:** `Promise` **Example:** ```typescript const integrations = await client.integration.list(); ``` ### `client.integration.get(integrationId)` Get a specific integration by ID. **Parameters:** - `integrationId: string` **Returns:** `Promise` **Example:** ```typescript const integration = await client.integration.get("int_abc123"); ``` ### `client.integration.update(integrationId, input)` Update an integration. **Parameters:** - `integrationId: string` - `input: UpdateIntegrationInput` **Returns:** `Promise` **Example:** ```typescript const updated = await client.integration.update("int_abc123", { name: "Updated Name", apiKey: process.env.NEW_API_KEY, }); ``` ### `client.integration.delete(integrationId)` Delete an integration. **Parameters:** - `integrationId: string` **Returns:** `Promise<{ success: boolean }>` **Example:** ```typescript await client.integration.delete("int_abc123"); ``` ## Safe Methods All methods have corresponding `*Safe` variants that return `Result` instead of throwing: - `client.integration.listSafe()` - `client.integration.createSafe(input)` - `client.integration.getSafe(integrationId)` - `client.integration.updateSafe(integrationId, input)` - `client.integration.deleteSafe(integrationId)` **Example:** ```typescript const result = await client.integration.createSafe({ name: "Devin", provider: "devin", apiKey: process.env.DEVIN_API_KEY, }); if (result.error) { console.error(result.error.message); } else { console.log(result.data.id); } ``` ## Types ### IntegrationProvider ```typescript type IntegrationProvider = "devin" | "cursor"; ``` ### CreateIntegrationInput ```typescript type CreateIntegrationInput = { name: string; provider: IntegrationProvider; apiKey: string; metadata?: Record; }; ``` ### UpdateIntegrationInput ```typescript type UpdateIntegrationInput = { name?: string; apiKey?: string; metadata?: Record; }; ``` ### Integration ```typescript type Integration = { id: string; organizationId: string; name: string; provider: IntegrationProvider; apiKey: string; // Always returns "***" for security metadata: Record | null; createdBy: string; createdAt: string; updatedAt: string; deletedAt: string | null; }; ``` ## HTTP Endpoints ### Create Integration ``` POST /v0/integrations ``` **Request Body:** ```json { "name": "Devin Production", "provider": "devin", "apiKey": "devin_api_key_here", "metadata": { "environment": "production" } } ``` ### List Integrations ``` GET /v0/integrations ``` ### Get Integration ``` GET /v0/integrations/:integrationId ``` ### Update Integration ``` PUT /v0/integrations/:integrationId ``` **Request Body:** ```json { "name": "Updated Name", "apiKey": "new_api_key_here", "metadata": { "version": "v2" } } ``` ### Delete Integration ``` DELETE /v0/integrations/:integrationId ``` ## Authentication All API requests require authentication using your API key: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.beeps.dev/v0/integrations ``` --- # Members API Reference > Source: https://beeps.dev/docs/members/api/ import { Tabs, TabItem } from '@astrojs/starlight/components'; List organization members to resolve emails to user IDs when managing schedules. ## List Members ### `client.member.list()` List all members in your organization. **Returns:** `Promise` **Example:** ```typescript const members = await client.member.list(); ``` ### `client.member.listSafe()` List members without throwing exceptions. **Returns:** `Promise>` **Example:** ```typescript const result = await client.member.listSafe(); if (result.error) { console.error("Error:", result.error.message); } else { console.log(`Members: ${result.data.length}`); } ``` ### HTTP Endpoint ``` GET /v0/members ``` All API requests require authentication using your API key: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ https://api.beeps.dev/v0/members ``` ```bash beeps member list ``` Add `--json` for machine-readable output: ```bash beeps member list --json ``` **Tool:** `beeps_list_members` ```json {} ``` ## Types ### OrganizationMember ```typescript type OrganizationMember = { userId: string; organizationId: string; role: string; email: string; }; ``` --- # Troubleshooting > Source: https://beeps.dev/docs/troubleshooting/ Solutions for common beeps issues. ## Alerts are not appearing If you're sending alerts but they're not showing up: 1. Check the webhook URL — make sure you're POSTing to the right relay webhook 2. Check the payload — `title` is required, `severity` is recommended 3. Check the response — a `200` with an `alert_id` means beeps received it. A `401` means the webhook key is wrong. 4. For Sentry/Datadog/Axiom, check their delivery logs to see if the webhook is actually firing Quick test from your terminal: ```bash curl -X POST https://hooks.beeps.dev/YOUR_WEBHOOK_ID \ -H "Content-Type: application/json" \ -d '{"title": "Test Alert", "severity": "high"}' ``` ## Alerts appear but no one is notified Check these in order: 1. **Does the on-call user have contact methods?** Open the dashboard and check. Users without email or SMS configured won't get anything. 2. **Is someone actually on-call?** If your schedule has no members, `schedule_notify` rules have nothing to do. 3. **Are the relay rules enabled?** Rules are created with `enabled: true` by default, but check that someone didn't disable them. 4. **Is it the right rule type?** A `webhook` rule posts to a URL — it won't send email/SMS. You need a `schedule_notify` rule for that. ## No one is on-call The schedule probably has no members. Add at least one user to the rotation. If there are members but no one shows as on-call, check these next: 1. `startAt` may be in the future. A schedule is inactive until its start time. 2. `handoffTime` and `handoffDay` may not be what you expect. All times are UTC. ## AI agent never responds 1. **Check the integration exists** — the agent rule references an `integrationId`. Make sure that integration hasn't been deleted. 2. **Check the API key** — test the Devin/Cursor API key directly against their API to confirm it's valid and has the right permissions. 3. **Check the rule is enabled** — disabled rules are skipped. 4. **Check the rule's group and order** — if the agent rule is in the same group as a `schedule_notify` rule with a lower order, and a human responds first, the relay may stop before reaching the agent rule. ## Relay rules not firing 1. **Is the relay receiving alerts?** Check the dashboard for alerts on this relay. If there are none, the webhook isn't being called. 2. **Are the rules enabled?** Disabled rules are skipped entirely. 3. **Check the rule config** — each rule type has required fields: - `schedule_notify` needs a `scheduleId` - `webhook` needs an `endpoint` and `method` - `agent` needs an `agentType` and `integrationId` Invalid rule config now returns field-level validation issues, including unknown fields and typo suggestions when available. 4. **Check groups and order** — rules in the same group run sequentially by `order`. If an earlier rule in the same group causes a response, later rules may not fire. ## Getting help If none of the above helps, reach out with: - Your organization ID - The relay ID and rule IDs involved - An example alert payload - The error message or unexpected behavior you're seeing - The `X-Request-ID` response header if you have it --- # Best Practices > Source: https://beeps.dev/docs/best-practices/ ## Idempotent Configuration Always set `externalKey` on relays, schedules, and relay rules. This makes operations idempotent—you can safely re-run setup scripts without creating duplicates. ```typescript // First run: creates relay const relay = await client.relay.create({ name: "Production Alerts", externalKey: "prod-alerts-v1", }); // Second run: updates existing relay (no duplicate) const sameRelay = await client.relay.create({ name: "Production Alerts (Updated)", externalKey: "prod-alerts-v1", // Same key = update }); ``` Use a consistent naming convention for external keys: ```typescript // Recommended: namespace::resource::identifier "myco::relay::production" "myco::schedule::primary-weekly" "myco::rule::notify-oncall" ``` ## Parallel vs Sequential Execution The `group` field on relay rules controls execution flow: - **Same group** = sequential (order 1, then 2, then 3) - **Different groups** = parallel (run simultaneously) ```typescript // Parallel: AI and humans notified at the same time { group: "agents", order: 1, ruleType: "agent" } { group: "humans", order: 1, ruleType: "schedule_notify" } // Sequential: AI tries first, then escalates to humans { group: "default", order: 1, ruleType: "agent" } { group: "default", order: 2, ruleType: "schedule_notify" } ``` **Use parallel when:** You want the fastest possible response time. **Use sequential when:** You want AI to attempt resolution before interrupting humans. ## Relays - **Descriptive names**: Use names that indicate purpose ("Production Alerts", not "Relay 1") - **Separate by service**: Create different relays for different services or environments - **Test rules disabled**: Create rules with `enabled: false`, test, then enable ## Schedules - **UTC times**: All times are in UTC. Account for your team's time zones when setting `handoffTime` - **Default start**: Omit `startAt` when you want a schedule to start immediately. Set it explicitly for planned cutovers. - **Member order matters**: Members rotate in the order they're added - **Weekly over daily**: Weekly rotations are generally better for work-life balance - **Always have coverage**: Ensure your schedule has at least one member at all times ## Contact Methods - **Multiple channels**: Add both email and SMS for each on-call engineer - **E.164 format**: Use international format for phone numbers (+14155551234) - **Verify methods**: Verified contact methods have higher deliverability - **Keep updated**: Review contact information when team members change roles ## Agent Integrations - **Environment separation**: Use separate integrations for dev/staging/prod - **Rotate keys regularly**: Update API keys periodically for security - **Descriptive metadata**: Store environment and purpose in metadata ## Testing 1. Create relays and rules with `enabled: false` 2. Send test alerts to verify webhook connectivity 3. Use `client.relay.simulate()` to preview routing 4. Use `client.relay.lint()` to validate coverage 5. Enable rules once verified --- # Terminal > Source: https://beeps.dev/docs/using-beeps/terminal/ import { Tabs, TabItem } from "@astrojs/starlight/components"; beeps-terminal is a keyboard-driven TUI dashboard that gives you a live view of active alerts, alert details, and your on-call roster — all from the terminal. ## Installation ```bash npm install -g beeps-terminal ``` Then run: ```bash beeps-terminal ``` ```bash npx beeps-terminal ``` ## Configuration beeps-terminal needs one environment variable: | Variable | Required | Description | |----------|----------|-------------| | `BEEPS_API_KEY` | Yes | Your API key from organization settings (looks like `bk_xxxxxxxx`) | ```bash export BEEPS_API_KEY="bk_your_api_key_here" beeps-terminal ``` ## Layout The dashboard has a 3-panel layout that adapts to your terminal size: ``` ┌──────────────────────┬──────────────────────┐ │ │ │ │ Active Alerts │ Alert Detail / │ │ │ Recent Deploys │ │ (scrollable list │ │ │ of all active ├──────────────────────┤ │ alerts) │ │ │ │ On-Call Roster │ │ │ │ └──────────────────────┴──────────────────────┘ ``` - **Left pane** — scrollable list of active alerts with status, severity, source, and assignment info. Updates automatically. - **Top-right pane** — shows recent deploy activity by default. Press `enter` on an alert to expand its full detail (ID, status, severity, timestamps, metadata). - **Bottom-right pane** — on-call roster showing who's on-call for each schedule, their active alert count, rotation end time, and who's up next. ## Keybindings | Key | Action | |-----|--------| | `↑` / `↓` | Navigate alerts | | `enter` | Expand alert detail | | `escape` | Collapse detail | | `a` | Acknowledge alert | | `r` | Resolve alert | | `e` | Escalate alert | | `q` / `ctrl+c` | Quit | --- # MCP Server > Source: https://beeps.dev/docs/using-beeps/mcp-server/ import { Tabs, TabItem } from "@astrojs/starlight/components"; The beeps MCP server lets you triage, fix, and resolve alerts without leaving your editor. ## Setup ```bash claude mcp add --transport http --scope user beeps https://mcp.beeps.dev/mcp ``` Verify: ```bash claude mcp list claude mcp get beeps ``` #### Optional: Pre-Registered OAuth Client If your deployment disables open dynamic client registration, add a client id/secret: ```bash claude mcp add --transport http --scope user --client-id --client-secret beeps https://mcp.beeps.dev/mcp ``` ```bash codex mcp add beeps --url https://mcp.beeps.dev/mcp ``` Run OAuth login: ```bash codex mcp login beeps --scopes "openid,profile,email,offline_access,beeps.tools.read,beeps.tools.write" ``` Verify: ```bash codex mcp list codex mcp get beeps ``` ## Tools ### Triage **`beeps_list_active_alerts`** — List active unresolved alerts with optional severity filter. ```text Show me all active alerts ``` ```text Are there any critical alerts right now? ``` **`beeps_list_alert_responders`** — List alert responders and their status. ```text Who's responding to alert al_abc123? ``` **`beeps_get_fix_status`** — Get current fix progress by alert jobs or a specific job. ```text What's the fix status on alert al_abc123? ``` ### Act **`beeps_on_it`** — Mark an alert as being worked by a responder. ```text I'm on it for alert al_abc123 ``` **`beeps_fix_here`** — Load complete alert fix context for foreground remediation. ```text Load the fix context for alert al_abc123 and help me resolve it ``` ### Resolve **`beeps_update_responder_status`** — Mark responder as done or dropped and optionally attach a PR URL. ```text Mark my response to al_abc123 as done with PR https://github.com/org/repo/pull/42 ``` **`beeps_resolve_alert`** — Resolve an alert. ```text Resolve alert al_abc123 ``` ### Scheduling **`beeps_get_on_call`** — Get the currently on-call user for a schedule. ```text Who's on call for the platform schedule? ``` **`beeps_set_override`** — Set a temporary schedule override. ```text Override the platform schedule to put me on call from now until 6pm ``` **`beeps_update_override`** — Update the time window or reason of an override. ```text Extend the current override until Monday ``` **`beeps_cancel_override`** — Cancel an override to restore normal rotation. ```text Cancel the override on the platform schedule ``` **`beeps_get_schedule_assignments`** — Get upcoming schedule assignments. ```text Show the next 5 on-call assignments for the platform schedule ``` ### Configuration **`beeps_list_relays`** — List all relays. ```text What relays do we have? ``` **`beeps_list_webhooks`** — List webhooks for a relay. ```text What webhook URLs does the production relay have? ``` **`beeps_create_relay`** — Create a new relay. ```text Create a relay called "staging alerts" ``` **`beeps_list_schedules`** — List all schedules. ```text Show me all schedules ``` **`beeps_create_schedule`** — Create a schedule. ```text Create a weekly schedule on the production relay, handoff Monday at 9am ``` **`beeps_list_schedule_members`** — List members of a schedule. ```text Who's in the primary on-call rotation? ``` **`beeps_add_schedule_member`** — Add a member to a rotation. ```text Add alice@example.com to the primary schedule ``` **`beeps_remove_schedule_member`** — Remove a member from a rotation. ```text Remove Bob from the primary schedule ``` **`beeps_list_relay_rules`** — List rules for a relay. ```text What rules are configured on the production relay? ``` **`beeps_create_relay_rule`** — Create a relay rule. ```text Add a schedule_notify rule to the production relay for the primary schedule ``` **`beeps_delete_relay_rule`** — Delete a relay rule. ```text Delete the webhook rule on the staging relay ``` **`beeps_assign_alert`** — Assign an alert to a user. ```text Assign alert alr_abc123 to Alice ``` **`beeps_list_integrations`** — List integrations. ```text What integrations are configured? ``` **`beeps_list_members`** — List organization members. ```text Who's in the org? ``` **`beeps_get_alert`** — Get full details of a specific alert. ```text Show me the details of alert alr_abc123 ``` ## Troubleshooting ### `403 missing required scope` Re-run `codex mcp login beeps` with both `beeps.tools.read` and `beeps.tools.write`. For Claude Code, remove/re-add the server and complete OAuth consent again. ### `Organization context required` If the user belongs to multiple organizations, set an active org in the web app first. For Claude Code, you can also set a fixed header when adding the server: ```bash claude mcp add --transport http --scope user -H "X-Organization-Id: " beeps https://mcp.beeps.dev/mcp ``` ### `explicit write confirmation required` Write tools require confirmation metadata by default. Ask the agent to include MCP `_meta`: ```json { "beeps/confirmWrite": true } ``` For self-hosted environments, this policy can be disabled with `BEEPS_REQUIRE_WRITE_CONFIRMATION=false`. --- # CLI > Source: https://beeps.dev/docs/using-beeps/cli/ import { Tabs, TabItem } from "@astrojs/starlight/components"; The beeps CLI lets you manage relays, schedules, alerts, and integrations directly from the command line — or define your entire on-call setup as code. ## Installation ```bash npm install -g @beepsdev/cli ``` ```bash npx @beepsdev/cli --help ``` ## Authentication Set your API key as an environment variable: ```bash export BEEPS_API_KEY="bk_your_api_key_here" ``` Or use a JSON config file: ```json { "apiKey": "bk_your_api_key_here", "baseUrl": "https://api.beeps.dev/v0" } ``` ```bash beeps relay list --config ./beeps.cli.json # or set once via environment variable export BEEPS_CLI_CONFIG="./beeps.cli.json" ``` Resolution order (highest priority first): `--api-key` flag > config file > `BEEPS_API_KEY` environment variable. ## Global Options These flags are available on every command: | Flag | Description | |------|-------------| | `--api-key ` | API key for authentication | | `--base-url ` | Custom API endpoint | | `--config ` | Path to a JSON config file | | `--timeout ` | Request timeout in milliseconds (default: 10000) | | `--retries ` | Number of retry attempts (default: 2) | | `--json` | Output machine-readable JSON | | `--help` | Show usage info | ## Quick Commands ### See who's on call ```bash beeps schedule on-call --schedule-id sch_abc123 ``` ### List active alerts ```bash beeps alert list --active ``` ### Respond to an alert ```bash beeps alert on-it --alert-id alt_abc123 ``` ### Resolve an alert ```bash beeps alert resolve --alert-id alt_abc123 ``` ### List relays ```bash beeps relay list ``` ### Simulate a relay ```bash beeps relay simulate --relay-id rly_abc123 --simulate-at 2025-03-01T00:00:00Z ``` ### Define your setup as code ```bash beeps relay export -o beeps.config.ts # export current setup beeps relay plan -f beeps.config.ts # preview changes beeps relay apply -f beeps.config.ts # apply changes ``` ## Command Reference ``` beeps ├── alert │ ├── list [--active] [--resolved] │ ├── get --alert-id │ ├── on-it --alert-id [--user-id ] │ ├── assign --alert-id --user-id │ ├── resolve --alert-id │ ├── responders --alert-id │ ├── agents --alert-id │ └── fix-context --alert-id ├── relay │ ├── list │ ├── create --name [--external-key ] │ ├── lint --relay-id [--coverage-days ] │ ├── simulate --relay-id --simulate-at │ ├── plan -f │ ├── apply -f [--prune] [--dry-run] │ ├── export -o │ └── rule │ ├── list --relay-id │ ├── get --relay-id --rule-id │ ├── create --relay-id --name --rule-type │ └── delete --relay-id --rule-id ├── schedule │ ├── list │ ├── create --name --relay-id --type ... │ ├── on-call --schedule-id │ ├── assignments --schedule-id [--count ] │ ├── add-member --schedule-id (--email | --user-id ) │ ├── remove-member --schedule-id --user-id │ └── override │ ├── create --schedule-id --user-id --start-at --end-at │ ├── list --schedule-id │ ├── update --schedule-id --override-id │ └── cancel --schedule-id --override-id ├── integration │ ├── list │ ├── get --integration-id │ ├── create --provider --name --api-key-env │ ├── update --integration-id │ └── delete --integration-id ├── webhook │ └── list --relay-id ├── agent-job │ ├── list │ ├── get --job-id │ └── status --job-id ├── member │ └── list └── config └── lint -f ``` ## Reference - [Config-as-Code](/docs/cli/config) — define relays, schedules, and rules in TypeScript - [Relay Commands](/docs/cli/relay) — manage relays and rules directly - [Schedule Commands](/docs/cli/schedule) — manage schedules, members, and overrides - [Alert Commands](/docs/cli/alert) — triage, respond to, and resolve alerts - [Integration Commands](/docs/cli/integration) — connect AI agents and notification services All commands support `--json` for machine-readable output and `--help` for usage info.