# Alarum, Integration guide for LLM agents (Claude Code & co.) Alarum is a centralized webhook/event hub. Scripts, CI pipelines, cron jobs and services send "events" to Alarum over a single HTTP POST. The user reads them in a unified inbox, gets push notifications, and can re-forward them to Discord or Slack. This file explains how to integrate Alarum event-sending into a user's project. ## Getting a token Tokens are created by the human user in the Alarum UI (a project's Settings → Tokens, or its Integration tab). A token looks like `alrm_...` and belongs to exactly one project. You do NOT create tokens yourself, there is no management API. Ask the user to create a project and a token, then hand you the token. Treat it as a secret: environment variable or CI secret, never committed. There is a second, different kind of token for READING events back, `alrm_at_...`, scoped to the account instead of a project. See "Reading events back" below. Everything until then is about sending. ## Ingestion endpoint ``` POST https://alarum.me/h Authorization: Bearer Content-Type: application/json ``` The token goes in the `Authorization: Bearer ` header (it stays out of access logs and shell history). The request body is a JSON object, the event payload (schema below). Response codes: - `204 No Content`, event accepted - `400 Bad Request`, invalid payload (bad JSON, missing title, invalid level) - `401 Unauthorized`, unknown or revoked token - `429 Too Many Requests`, rate limit hit (1000 events/hour/token); includes a `Retry-After` header ## Payload schema (alarum native format) Only `title` is required. Everything else is optional. | Field | Type | Required | Notes | |-------------|------------|----------|--------------------------------------------------------------| | `level` | enum | no | `info` \| `success` \| `warn` \| `error` \| `critical`, default `info` | | `title` | string | **yes** | max 256 chars | | `message` | string | no | max 8000 chars, multi-line OK | | `tags` | string[] | no | max 16, each max 32 chars, slugified | | `source` | string | no | max 64, e.g. `github-actions`, `cron-backup` | | `url` | string | no | max 1024, link shown in the event detail | | `fields` | object[] | no | max 25, each `{ name, value, inline }`, Discord-embed style | | `timestamp` | string | no | ISO 8601, event time; defaults to received time | | `footer` | string | no | max 256 | | `author` | object | no | `{ name, url }` | | `color` | string | no | `#RRGGBB` hex, overrides the level color | Example body: ```json { "level": "success", "title": "Deploy succeeded", "message": "Build #42 is live on production.", "source": "github-actions", "tags": ["deploy", "prod"], "url": "https://github.com/example/repo/actions", "fields": [ { "name": "Commit", "value": "a1b2c3d", "inline": true }, { "name": "Duration", "value": "3m12s", "inline": true } ] } ``` ## Levels & semantics | Level | Meaning | Push notification | |------------|----------------------------------|---------------------------------------| | `info` | Neutral informational event. | OFF by default | | `success` | Something completed OK. | ON by default | | `warn` | Needs attention but not broken. | ON by default | | `error` | Something failed. | ON by default | | `critical` | Urgent failure. | ON; can bypass the user's quiet hours | Pick the level that matches real severity. Do not send everything as `error`. ## Accepted formats Alarum auto-detects the payload format: - `alarum`, the native schema above. Use this. - `discord`, a Discord webhook payload (`{ "embeds": [...] }`) is accepted and normalized automatically. - `raw`, any other JSON is stored as-is in the `message` field. You can force the format with a header: `X-Alarum-Format: alarum|discord|raw`. ## Examples cURL: ```bash curl -X POST 'https://alarum.me/h' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"level":"success","title":"Deploy succeeded","message":"Build #42 is live","source":"github-actions","tags":["deploy","prod"]}' ``` JavaScript (fetch): ```js await fetch('https://alarum.me/h', { method: 'POST', headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json', }, body: JSON.stringify({ level: 'error', title: 'Job failed', message: '...' }), }) ``` Python (requests): ```python import requests requests.post( 'https://alarum.me/h', json={'level': 'warn', 'title': 'Disk almost full', 'message': '92% used'}, headers={'Authorization': 'Bearer '}, ) ``` GitHub Actions (a step in `.github/workflows/*.yml`): ```yaml - name: Notify Alarum run: | curl -X POST 'https://alarum.me/h' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"level":"success","title":"CI passed"}' ``` Codeberg CI / Woodpecker (`.woodpecker.yml`): ```yaml steps: notify-alarum: image: curlimages/curl commands: - | curl -X POST 'https://alarum.me/h' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{"level":"success","title":"Pipeline OK"}' ``` GitLab CI (`.gitlab-ci.yml`): ```yaml notify-alarum: image: curlimages/curl:latest script: - | curl -X POST 'https://alarum.me/h' \ -H "Authorization: Bearer $ALARUM_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"level":"success","title":"Pipeline OK","source":"gitlab-ci"}' ``` Node.js (axios): ```js import axios from 'axios' await axios.post( 'https://alarum.me/h', { level: 'error', title: 'Job failed', message: 'See logs' }, { headers: { Authorization: 'Bearer ' } }, ) ``` Go: ```go package main import ( "bytes" "net/http" ) func notify() error { body := bytes.NewBufferString(`{"level":"error","title":"DB connection lost"}`) req, _ := http.NewRequest("POST", "https://alarum.me/h", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") _, err := http.DefaultClient.Do(req) return err } ``` PHP: ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'level' => 'warn', 'title' => 'Slow query', ]), CURLOPT_RETURNTRANSFER => true, ]); curl_exec($ch); curl_close($ch); ``` Ruby: ```ruby require 'net/http' require 'json' uri = URI('https://alarum.me/h') req = Net::HTTP::Post.new(uri) req['Authorization'] = 'Bearer ' req['Content-Type'] = 'application/json' req.body = { level: 'success', title: 'Deploy ok' }.to_json Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } ``` PowerShell: ```powershell $headers = @{ Authorization = 'Bearer '; 'Content-Type' = 'application/json' } $body = @{ level = 'error'; title = 'Service stopped'; source = $env:COMPUTERNAME } | ConvertTo-Json Invoke-RestMethod -Uri 'https://alarum.me/h' -Method Post -Headers $headers -Body $body ``` Generic bash script (cron-friendly): ```bash #!/usr/bin/env bash # alarum-notify.sh [message] LEVEL=${1:-info}; TITLE=$2; MSG=${3:-} PAYLOAD=$(jq -nc --arg l "$LEVEL" --arg t "$TITLE" --arg m "$MSG" \ '{level:$l,title:$t,message:$m}') curl -fsS -X POST 'https://alarum.me/h' \ -H "Authorization: Bearer ${ALARUM_TOKEN}" \ -H 'Content-Type: application/json' \ -d "$PAYLOAD" ``` ## Reading events back (agent API, /api/v1) Everything above SENDS events. There is also a read API, for a client that wants to show a user's events outside the browser (a terminal panel, a status line, a CLI). It authenticates with an ACCOUNT token, not an ingestion token: `alrm_at_...`, created by the human under Settings → Your agent. It is scoped to the whole account, so it reads every project that account can reach, and it can mark events read. **It is a paid-plan feature**: on Discover every call answers `403 plan_required`. Do not confuse the two kinds of token. `alrm_...` is an ingestion secret bound to one project and can only POST to `/h`. `alrm_at_...` can read everything its owner can see. Never put an `alrm_at_...` token in a CI job. ``` GET https://alarum.me/api/v1/events?limit=20 Authorization: Bearer alrm_at_... ``` Endpoints: - `GET /api/v1/me`, who the token belongs to, the plan, and the rate limit. One call to check a token at startup. - `GET /api/v1/events`, newest first. Filters: `project`, `workspace` (ids), `level` (comma-separated), `tag`, `source`, `unread=1`, `from`, `to` (unix seconds), `limit` (1-200, default 50), `before` (the cursor, from `nextCursor`). Answers `{ events, nextCursor, hasMore }`; each event carries its `project` and a `webUrl`. - `GET /api/v1/events/<id>`, one event, plus its `rawPayload`. - `POST /api/v1/events/<id>/read` and `DELETE` on the same path, mark it read or unread. Idempotent. It is recorded as the token's OWNER, so the web inbox stays in step. - `GET /api/v1/projects` and `GET /api/v1/workspaces`, what the token can reach, with unread counts, to offer a scope selector. Each project also carries `push`, the levels the account asked to be notified about for it (`{ info, success, warn, error, critical }`, booleans): announce on those rather than inventing your own threshold. Quiet hours are not part of it, they protect a phone at night and a terminal reader is at work. Response codes: `401` (missing or revoked token, with `code: missing_token | invalid_token`), `403 plan_required`, `403` (a project or workspace the account cannot reach), `404` (unknown id), `429` (per-minute cap, `Retry-After` plus `retryAfter` in the body). Polling: keep the `receivedAt` of the newest event seen and ask for `from=<that>+1`. The steady state is an empty list. 30 seconds is the cadence the web app itself uses; faster buys nothing (events are not pushed to this API) and eats into the per-minute cap. ## Best practices - Send events at meaningful checkpoints: deploy done, build failed, backup completed, cron ran, an error was caught. - One token per source, the user can revoke a token without affecting others. - Give a clear, human-readable `title`: it is what shows in the inbox list and in the push notification. - Use `source` to identify where the event came from (CI name, script name). - Use `tags` for filtering (`prod`, `staging`, `deploy`, ...). - Don't spam, the limit is 1000 events/hour/token. Batch or throttle noisy sources. - The token is a secret. Env var or CI secret only, never commit it. ## No management API (yet) Events can be sent (`/h`) and read (`/api/v1`), but nothing can be CREATED or CONFIGURED through an API: no projects, no tokens, no forwarders, no notification rules. All of that is managed by the human in the Alarum UI. If you need a project or a token, ask the user to create it and give it to you.