AWS CloudWatch — Ingest Setup

This guide shows how to route AWS CloudWatch alarms to Culprit. CloudWatch does not POST to webhooks directly — it publishes to an Amazon SNS topic, and SNS delivers the alarm to an HTTPS subscription. You point that subscription at your Culprit endpoint.


Endpoint

POST https://ingest.theculprit.ai/ingest/<tenant_id>/<service_name>/cloudwatch?token=<ingest_token>

Replace <tenant_id> and <service_name> with the values shown on your service's settings page, and <ingest_token> with your ingest token.


Authentication

Unlike other integrations, SNS cannot send custom headers, so the ingest token rides in the URL query string as ?token=<ingest_token> instead of an Authorization header.

Getting your ingest token:

  1. Open the Culprit dashboard → Services → select the service.
  2. In the Ingest token section, click Issue.
  3. Copy the token shown — it is displayed once. After you navigate away, only the token prefix is shown.

To rotate a compromised token, click Rotate. The previous token remains valid for the grace period shown, giving you time to update the SNS subscription URL without an interruption.

Treat the full subscription URL as a secret. Because the token is in the query string, anyone who can read the URL can post to your endpoint. Culprit additionally verifies the AWS SNS message signature on every notification as a second layer of defense, but you should still keep the URL confidential.


Setup in AWS

  1. Create (or reuse) an SNS topic. In the SNS console, create a standard topic — for example culprit-alarms.
  2. Create an HTTPS subscription on that topic:
    • Protocol: HTTPS
    • Endpoint: your Culprit endpoint, including the ?token= query string shown above.
  3. Confirm the subscription. SNS immediately sends a SubscriptionConfirmation message to the endpoint. Culprit confirms it automatically — you do not need to click the confirmation link manually. Within a few seconds the subscription status in the SNS console changes from Pending confirmation to Confirmed.
  4. Point your CloudWatch alarms at the topic. For each alarm, set the Notification action (for the In alarm, OK, and optionally Insufficient data states) to publish to the SNS topic you created.

That's it. When an alarm changes state, CloudWatch publishes to SNS, SNS posts the notification to Culprit, Culprit verifies the SNS signature, and the alarm flows into your incident pipeline.


How Culprit maps CloudWatch alarms

CloudWatch alarm notifications arrive wrapped in an SNS envelope. Culprit unwraps the envelope, verifies its signature, and maps the inner alarm.

Field mapping

| CloudWatch field | Culprit event field | Notes | |---|---|---| | AlarmName | event_type | Falls back to cloudwatch.alarm if absent | | NewStateValue | severity | ALARM → HIGH; INSUFFICIENT_DATA → MEDIUM; OK → LOW | | NewStateReason | message | The human-readable reason for the state change | | AlarmDescription | message fallback | Used when NewStateReason is absent | | Region | context | Preserved for correlation and RCA | | (entire alarm) | context.raw | The full alarm object is preserved |

Severity resolution

| NewStateValue | Culprit severity | |---|---| | ALARM | HIGH | | INSUFFICIENT_DATA | MEDIUM | | OK (or any other value) | LOW |

OK maps to LOW because it is a recovery/resolution signal — it should not page as if it were a new problem.

PII in any field (IP addresses, hostnames, email addresses, API keys) — for example a hostname embedded in NewStateReason — is detected and tokenized before any storage or notification. The original values are preserved encrypted and are visible only in the incident detail view after authentication.


Worked example

CloudWatch publishes an alarm. SNS delivers this notification body:

{
  "Type": "Notification",
  "MessageId": "abc-123",
  "TopicArn": "arn:aws:sns:us-east-1:111122223333:culprit-alarms",
  "Subject": "ALARM: \"checkout-5xx\" in US East (N. Virginia)",
  "Message": "{\"AlarmName\":\"checkout-5xx\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 5xx > 50 for 2 datapoints on api-1.acme.internal\",\"AlarmDescription\":\"5xx errors on checkout\",\"Region\":\"us-east-1\"}",
  "Timestamp": "2026-05-28T00:00:00.000Z",
  "SignatureVersion": "1",
  "Signature": "...",
  "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-....pem"
}

Culprit event (before tokenization):

{
  "event_type": "checkout-5xx",
  "severity": "high",
  "source": "cloudwatch",
  "message": "Threshold Crossed: 5xx > 50 for 2 datapoints on api-1.acme.internal",
  "context": {
    "AlarmName": "checkout-5xx",
    "NewStateValue": "ALARM",
    "NewStateReason": "Threshold Crossed: 5xx > 50 for 2 datapoints on api-1.acme.internal",
    "Region": "us-east-1"
  }
}

This event flows through the standard pipeline: encrypted vault storage → PII tokenization → storm check → embedding → correlation → root-cause analysis.


Successful response

For an alarm notification:

{ "accepted": true, "ids": ["<vault_id>"] }

For the one-time subscription-confirmation handshake:

{ "confirmed": true }

Error responses

| Status | Body | Meaning | |---|---|---| | 401 | { "error": "missing_auth" } | No ?token= query parameter | | 401 | { "error": "invalid_ingest_token" } | Token is incorrect or has been rotated/revoked | | 401 | { "error": "invalid_sns_signature" } | The SNS message signature did not verify (not a genuine AWS SNS message, or the signing certificate could not be validated) | | 400 | { "error": "invalid_subscribe_url" } | A subscription-confirmation message carried a SubscribeURL that is not an AWS host | | 400 | { "error": "unrecognized_payload" } | Body is not valid JSON, is not a recognized SNS envelope, or its inner message is not a CloudWatch alarm | | 403 | { "error": "service_disabled" } | The service exists but monitoring is paused | | 404 | { "error": "service_deleted_or_not_found" } | The service does not exist | | 429 | { "error": "rate_limited" } | Storm protection active (sustained high volume) |