Documentation

    Integrations

    There is one way in: an HTTP POST of a small JSON document to the team’s address. Every monitoring tool that can send a webhook with a payload you control can send it. This page has the exact configuration for the tools people ask about, and the outbound side.

    The one contract

    Every recipe below ends in the same request: POST https://api.igniteops.io/v1/organizations/{org}/incident-management/{team}/alerts, the team’s integration key in the Authorization header with no prefix, and a JSON body with at least title and message. severity (P1 to P5), alias for deduplication, source, entity, alert_url, tags and custom_fields are optional. The alerts page has every field and its limit.

    The team’s address is in the console: open the team, then Integration keys, and create a key named after the tool. {org} and {team} are the names in the console’s address bar.

    curl, cron and shell scripts

    One alert from a terminalbash
    curl -sS -X POST "https://api.igniteops.io/v1/organizations/{org}/incident-management/{team}/alerts" \
      -H "Authorization: $IGNITEOPS_KEY" \
      -H "Content-Type: application/json" \
      -d '{"title": "Nightly backup failed on db-02", "message": "pg_dump exited 1: disk full on /backups", "source": "cron", "severity": "P2", "alias": "backup-db-02"}'

    A cron job that pages when its command fails, with the last lines of the log as the message:

    /etc/cron.daily/backupbash
    #!/usr/bin/env bash
    # Runs the backup and pages the on-call if it fails. The key lives in the environment, never in the script.
    set -u
    if ! /opt/backup/run.sh > /var/log/backup.log 2>&1; then
      curl -sS -X POST "$IGNITEOPS_ALERTS_URL" \
        -H "Authorization: $IGNITEOPS_KEY" \
        -H "Content-Type: application/json" \
        -d "$(jq -n --arg m "$(tail -n 20 /var/log/backup.log)" \
              '{title: "Nightly backup failed on db-02", message: $m, source: "cron", severity: "P2", alias: "backup-db-02"}')"
    fi

    GitHub Actions

    Store the integration key as a repository secret and the team’s address as a variable, then add a last step that runs only when the job failed. The alias keeps repeated failures of the same branch on one alert.

    A failing deploy pages the on-callYAML
    # .github/workflows/deploy.yml
    - name: Page the on-call if the deploy failed
      if: failure()
      env:
        IGNITEOPS_ALERTS_URL: ${{ vars.IGNITEOPS_ALERTS_URL }}
        IGNITEOPS_KEY: ${{ secrets.IGNITEOPS_INTEGRATION_KEY }}
        RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
      run: |
        curl -sS -X POST "$IGNITEOPS_ALERTS_URL" \
          -H "Authorization: $IGNITEOPS_KEY" \
          -H "Content-Type: application/json" \
          -d "$(jq -n --arg t "Deploy failed: ${{ github.repository }} (${{ github.ref_name }})" \
                    --arg m "Workflow ${{ github.workflow }} failed. $RUN_URL" \
                    --arg a "deploy-${{ github.repository }}-${{ github.ref_name }}" \
                    --arg u "$RUN_URL" \
                    '{title: $t, message: $m, source: "github-actions", severity: "P2", alias: $a, alert_url: $u}')"

    The same pipeline can record the deployment itself, so the alerts that follow it are read next to it. With the same external_id, a start recorded before the deploy step is closed by this end. The full recipe and every field are on the deployments page.

    Record the deploymentYAML
    # .github/workflows/deploy.yml, after the deploy step
    - name: Record the deployment end
      if: always() && (steps.deploy.outcome == 'success' || steps.deploy.outcome == 'failure')
      continue-on-error: true
      env:
        IGNITEOPS_KEY: ${{ secrets.IGNITEOPS_INTEGRATION_KEY }}
        STATUS: ${{ steps.deploy.outcome == 'success' && 'succeeded' || 'failed' }}
      run: |
        jq -n --arg v "${{ github.sha }}" --arg s "$STATUS" --arg id "${{ github.run_id }}-${{ github.run_attempt }}-checkout-api" \
          '{service: "checkout-api", version: $v, status: $s, external_id: $id}' > "$RUNNER_TEMP/deployment-body"
        printf 'Authorization: %s\n' "$IGNITEOPS_KEY" | curl -sS --fail-with-body --retry 3 --retry-delay 5 \
          -X POST "https://api.igniteops.io/v1/organizations/acme/incident-management/platform/deployments" \
          -H @- -H "Content-Type: application/json" --data @"$RUNNER_TEMP/deployment-body" \
          || { echo; echo "::warning::The deployment end was not recorded in IgniteOps"; }

    Datadog

    Datadog’s Webhooks integration lets you set the request body and the headers, so no relay is needed. In Integrations → Webhooks, add a webhook named for example igniteops-platform, with the team’s address as the URL, this payload, and the custom headers below. Datadog fills the $VARIABLES when the monitor fires.

    PayloadJSON
    {
      "title": "$ALERT_TITLE",
      "message": "$TEXT_ONLY_MSG",
      "source": "datadog",
      "severity": "$ALERT_PRIORITY",
      "alias": "$ALERT_ID",
      "alert_url": "$LINK",
      "tags": { "datadog": "$TAGS" }
    }
    Custom headersJSON
    { "Authorization": "ign_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "Content-Type": "application/json" }
    • $ALERT_PRIORITY is the monitor’s priority (P1 to P5), which maps to IgniteOps severities as they are. Give your monitors a priority, or write a fixed "P2" instead.
    • $ALERT_ID as the alias folds every notification of one monitor into one alert.
    • Mention the webhook only in the alert branch of the monitor’s message, so recoveries and warnings do not post:
    In the monitor message
    {{#is_alert}}@webhook-igniteops-platform{{/is_alert}}

    Grafana

    Grafana’s webhook contact point can send a custom payload written as a template over the alert group, and extra HTTP headers. In Alerting → Contact points, add a Webhook contact point with the team’s address as the URL, an extra header Authorization whose value is the key (no scheme), and this payload under the optional settings. Tick Disable resolved message, since IgniteOps has no auto-resolve.

    Custom payloadGo template
    {
      "title": "{{ .CommonLabels.alertname }}{{ if .CommonLabels.instance }} on {{ .CommonLabels.instance }}{{ end }}",
      "message": "{{ .CommonAnnotations.summary }}\n{{ .CommonAnnotations.description }}",
      "source": "grafana",
      "severity": "{{ if eq .CommonLabels.severity \"critical\" }}P1{{ else if eq .CommonLabels.severity \"warning\" }}P3{{ else }}P2{{ end }}",
      "alias": "{{ .GroupKey }}",
      "alert_url": "{{ .ExternalURL }}"
    }
    • .GroupKey is stable for the group, so one firing group is one alert. If you prefer one alert per instance, set the notification policy to group by alertname and instance.
    • The severity mapping reads the severity label of your rules. Adjust the branches to your own labels.
    • On a Grafana that only offers the Bearer authorization fields and no extra headers, the key cannot be sent as is: use the small relay from the Alertmanager section below, which accepts the same payload shape Grafana sends by default.

    CloudWatch

    CloudWatch alarms publish their state changes to EventBridge, and an EventBridge API destination can POST straight to the team’s address with the key in a header. No Lambda is needed.

    • Create a connection with authorization type API key: key name Authorization, value the integration key.
    • Create an API destination on that connection: the team’s address as the endpoint, method POST.
    • Create a rule on the default bus with this event pattern (only the transitions into ALARM):
    Event patternJSON
    {
      "source": ["aws.cloudwatch"],
      "detail-type": ["CloudWatch Alarm State Change"],
      "detail": { "state": { "value": ["ALARM"] } }
    }
    • Add the API destination as the target, with an input transformer that builds the body:
    Input pathsJSON
    { "alarm": "$.detail.alarmName", "reason": "$.detail.state.reason", "region": "$.region" }
    Input templateJSON
    {
      "title": "CloudWatch alarm <alarm> is in ALARM",
      "message": "<reason>",
      "source": "cloudwatch",
      "severity": "P2",
      "alias": "cloudwatch-<alarm>",
      "alert_url": "https://console.aws.amazon.com/cloudwatch/home?region=<region>#alarmsV2:alarm/<alarm>"
    }

    The alarm’s name as the alias means a flapping alarm is one alert with a counter, not a page per evaluation.

    Prometheus Alertmanager

    Alertmanager’s webhook receiver sends a fixed JSON document (alerts[] with labels and annotations) and cannot put a bare key in the Authorization header, so it needs a small relay in between. This one maps each firing alert to an IgniteOps alert, keyed by Alertmanager’s own fingerprint:

    relay.mjsNode 20+
    // relay.mjs: a 30-line relay from Alertmanager's fixed webhook payload to IgniteOps.
    // Run it anywhere Alertmanager can reach: node relay.mjs, with IGNITEOPS_ALERTS_URL and IGNITEOPS_KEY in the environment.
    import http from 'node:http';
    
    const URL = process.env.IGNITEOPS_ALERTS_URL;
    const KEY = process.env.IGNITEOPS_KEY;
    const SEVERITY = { critical: 'P1', error: 'P2', warning: 'P3', info: 'P5' };
    
    http.createServer((req, res) => {
      let body = '';
      req.on('data', (chunk) => (body += chunk));
      req.on('end', async () => {
        const { alerts = [] } = JSON.parse(body || '{}');
        for (const a of alerts.filter((a) => a.status === 'firing')) {
          await fetch(URL, {
            method: 'POST',
            headers: { Authorization: KEY, 'Content-Type': 'application/json' },
            body: JSON.stringify({
              title: a.labels.alertname + (a.labels.instance ? ' on ' + a.labels.instance : ''),
              message: a.annotations.description || a.annotations.summary || JSON.stringify(a.labels),
              source: 'alertmanager',
              severity: SEVERITY[a.labels.severity] || 'P3',
              alias: a.fingerprint,
              alert_url: a.generatorURL,
              tags: a.labels,
            }),
          });
        }
        res.writeHead(204).end();
      });
    }).listen(8080);
    alertmanager.ymlYAML
    receivers:
      - name: igniteops-platform
        webhook_configs:
          - url: http://relay.internal:8080/
            send_resolved: false
    AlertmanagerIgniteOpsNotes
    labels.alertname (+ instance)titleWhat the page says.
    annotations.description or summarymessageThe body of the push and the email.
    labels.severityseveritycritical → P1, error → P2, warning → P3, info → P5 in the relay above. Change the map to taste.
    fingerprintaliasStable per alert, so repeats update one IgniteOps alert.
    generatorURLalert_urlBack to the expression in Prometheus.
    labelstagsEvery label becomes a tag that routing rules can match as tags.<label>.
    status: resolved(dropped)No auto-resolve yet. Close the alert when the work is done.

    Zapier, Make, Power Automate, n8n

    The flow tools send the same POST from a form, an email, a ticket or a calendar event. The no-code guide has the exact fields for each of them, and a Power Automate flow that drives a schedule from a shared Outlook calendar through overrides.

    Outbound: webhooks and Slack

    An escalation step can post to any URL or to a Slack incoming webhook, both on Pro and both included at no charge. A Slack step gets a formatted message with the title, the severity, the team and a link to the alert. Any other URL gets this JSON document:

    What a webhook step deliversHTTP
    POST <your URL>
    Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
    Content-Type: application/json
    
    {
      "event": "alert.escalation",
      "idempotency_key": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "sent_at": "2026-11-08T03:12:04.118Z",
      "organization": "acme",
      "team": "platform",
      "alert": {
        "alert_id": "0f1d2c3b-4a5e-4f60-9b7c-8d9e0f1a2b3c",
        "title": "Database connection pool exhausted on prod-db-01",
        "description": "PgBouncer reports 100% pool utilisation for 5 minutes.",
        "severity": "P1",
        "status": "open",
        "source": "datadog",
        "entity": "prod-db-01",
        "tags": { "env": "production", "service": "postgres" },
        "count": 3,
        "created_at": "2026-11-08T03:07:11.412Z",
        "url": "https://app.igniteops.io/org/acme/teams/platform/alerts/0f1d2c3b-…"
      },
      "escalation": { "execution_id": "…", "round": 0, "step": 3 }
    }
    • Every delivery carries an Idempotency-Key header, a UUID that names the page (the alert, the escalation, the round, the step and the URL). A retry after a timeout, a 408, 409, 429 or 5xx, or a step run again, carries the same key, so answer 2xx to a key you have already processed and drop the repeat. Keep keys for at least a day.
    • The request times out after 10 seconds. Answer quickly and do the work afterwards.
    • A webhook URL uses https on port 443 or http on port 80: a step with another port is refused when the policy is saved. A URL whose host resolves to a private or reserved address is never contacted.
    • Microsoft Teams, Discord and similar tools are wired through their own incoming-webhook URLs as a plain webhook step.

    Signed deliveries

    A webhook endpoint can have a signing secret, so your receiver can check that a request comes from IgniteOps and was not changed. Deliveries follow the Standard Webhooks scheme. Every request to a URL that is not Slack carries:

    HeaderValue
    webhook-idThe delivery’s id, the same value as Idempotency-Key, the same on every attempt.
    webhook-timestampThis attempt’s time, in Unix seconds.
    webhook-signatureOnce the endpoint has a secret: v1, then the base64 HMAC-SHA256 of "<webhook-id>.<webhook-timestamp>.<raw body>", keyed with the base64-decoded bytes after whsec_. One such value per valid secret, space-separated.
    Verify a deliveryNode.js
    import { createHmac, timingSafeEqual } from 'node:crypto';
    
    // secret: the whsec_… value; headers: the request's headers, lower-cased; rawBody: the body exactly as received
    export function verifyIgniteOps(secret, headers, rawBody) {
      const id = headers['webhook-id'];
      const timestamp = headers['webhook-timestamp'];
      const signatures = headers['webhook-signature'];
      if (!id || !timestamp || !signatures) return false;
      // Refuse old or future requests: 5 minutes either way
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
      const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
      const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest();
      // One "v1,<base64>" per valid secret, space-separated: accept any match
      return signatures.split(' ').some((entry) => {
        const [version, value] = entry.split(',');
        const given = Buffer.from(value || '', 'base64');
        return version === 'v1' && given.length === expected.length && timingSafeEqual(given, expected);
      });
    }
    • Verify over the raw body, before any JSON parsing, and accept the request when any of its signatures matches. Answer an unsigned or non-matching request with a 4xx: it is sent again later.
    • Creating and rotating: in the team’s On-call tab, under Escalation, the Webhook endpoints panel lists each webhook URL of the team’s policies and whether it is signed. A team or organization Owner or Manager selects Create secret or Rotate secret. The secret is shown once: store it in the receiver before closing the dialog. Requests carry its signature within about a minute.
    • A rotation keeps the previous secret signing beside the new one for up to 7 days (1 day by default, or stopped at once), so the receiver can switch without missing a request. During that time each request carries both signatures.
    • Send test, in the same panel, posts a test request to the endpoint and says whether it was delivered and whether it was signed. Enforce signatures in the receiver only once a test answers that the request was signed. Tests are limited to 5 a minute and 100 a day per organization.
    • Slack steps are never signed: Slack does not verify signatures. The URL is masked for everyone who cannot edit the policy.

    Checked against the product on November 8, 2026.