Table of Contents
- Why the usual SNS route gets awkward
- What you need
- Step 1 — Create a channel that rings
- Step 2 — Create the connection and API destination
- Step 3 — Create the rule and the input transformer
- Step 4 — Know what the event really looks like
- Step 5 — Filter down to what deserves a call
- Step 6 — Give recoveries a quieter door
- Test it before you trust it
- What about SNS and email-to-call?
- Limits that matter during an incident
- What this setup does not give you
- Troubleshooting
- Frequently asked questions
- Can CloudWatch call a phone number natively?
- Do I need an SNS topic?
- Will a CloudWatch alarm break through Do Not Disturb?
- Why not just subscribe the webhook URL to SNS?
- Does this cost anything on AWS?
- Can I call different people for different alarms?
- What about alarms in several regions or accounts?
- Wrap-up
- Related
A CloudWatch alarm cannot call your phone. Its only built-in actions are SNS, Auto Scaling, EC2 and Systems Manager, and SNS ends in email, SMS, or a queue you have to be watching. The tutorials that do get you a ringing phone wire up a Lambda function and a Twilio account. You can skip both: let EventBridge receive the alarm state change, reshape it into a small JSON body, and POST it to an Echobell calling channel.
This guide covers the whole path — the channel, the EventBridge rule, the input transformer, the filters that keep the ring meaningful, and why pointing SNS straight at the webhook looks like it should work and mostly does not.
Why the usual SNS route gets awkward
The default advice is "alarm → SNS topic → subscriber". For a phone call that has three problems:
- SNS has no voice protocol. Subscriber types are email, SMS, mobile push, SQS, Lambda, Firehose and HTTP(S). SMS is easy to miss and is blocked by Do Not Disturb; a voice call needs something you build yourself.
- The HTTPS message is a string inside a string. SNS delivers its envelope with
Content-Type: text/plain, and the alarm itself is a JSON document serialized into the envelope'sMessagefield. A webhook receiver that reads fields —AlarmName,NewStateValue— finds only one opaque blob. - The envelope is large. Between the signature, the signing certificate URL and the unsubscribe link, the envelope adds well over a kilobyte before your alarm text starts. Echobell truncates a notification body at 1500 bytes, so a "dump the whole body" template cuts off right where the useful part begins.
You can make SNS work with a Lambda that unwraps the message and forwards it. But CloudWatch already publishes every alarm state change to EventBridge as a clean JSON event — no action to configure — and EventBridge can reshape it and call an HTTPS endpoint itself.
What you need
- An AWS account and permission to create EventBridge rules, connections and API destinations
- Echobell installed (App Store / Google Play)
- At least one existing CloudWatch alarm
- Ten minutes
Nothing on your side has to accept inbound traffic. EventBridge makes the outbound request.
Step 1 — Create a channel that rings
In Echobell, create a channel and set its notification type to Calling. A calling channel behaves like an incoming call rather than a push, so it gets through iOS Focus Mode and Do Not Disturb.
The transformer in Step 3 sends a flat body with alarm, state, reason, region and link fields, so the templates stay short:
Title: {{state}}: {{alarm}}
Body: {{reason}}
Set the link template in advanced settings so tapping the notification opens the alarm in the console:
{{link}}
Copy the channel's webhook URL from the channel detail view:
https://hook.echobell.one/t/<channel-token>
Step 2 — Create the connection and API destination
EventBridge calls external HTTPS endpoints through an API destination, which needs a connection that carries authentication. Echobell authenticates by the secret token in the URL, so the connection is a formality — but EventBridge will not create a destination without one.
- Open EventBridge → Integration → API destinations → Connections → Create connection
- Name:
echobell - Authorization type: API key
- API key name:
x-source, Value:eventbridge - Create it, then go to API destinations → Create API destination
- Name:
echobell-calling - API destination endpoint: your Echobell channel URL from Step 1
- HTTP method:
POST - Invocation rate limit:
1per second is plenty and acts as a safety valve - Connection:
echobell
The API key header is sent on every request. You can check for it in an Echobell channel condition if you want a second gate in front of the token:
header["x-source"] == "eventbridge"
Step 3 — Create the rule and the input transformer
Go to EventBridge → Rules → Create rule in the same region as your alarms. Rules are regional; an alarm in eu-west-1 is invisible to a rule in us-east-1.
Use the default event bus and an event pattern rule. This pattern matches every alarm that moves into ALARM:
{
"source": ["aws.cloudwatch"],
"detail-type": ["CloudWatch Alarm State Change"],
"detail": {
"state": { "value": ["ALARM"] }
}
}
For the target, choose EventBridge API destination and pick echobell-calling. Let the console create a new execution role — it grants events:InvokeApiDestination for this destination and nothing else.
Then expand Additional settings → Configure target input → Input transformer:
Input path
{
"alarm": "$.detail.alarmName",
"state": "$.detail.state.value",
"reason": "$.detail.state.reason",
"region": "$.region",
"time": "$.time"
}
Template
{
"alarm": "<alarm>",
"state": "<state>",
"reason": "<reason>",
"region": "<region>",
"time": "<time>",
"link": "https://console.aws.amazon.com/cloudwatch/home?region=<region>#alarmsV2:alarm/<alarm>"
}
EventBridge sends the result as application/json, which is what lets Echobell read {{alarm}} and {{state}} directly.
Step 4 — Know what the event really looks like
The input path reads from the event CloudWatch publishes. Trimmed, it is:
{
"version": "0",
"id": "c4c1c1c9-6542-e61b-6ef0-8c4d36933a92",
"detail-type": "CloudWatch Alarm State Change",
"source": "aws.cloudwatch",
"account": "123456789012",
"time": "2026-10-16T03:12:44Z",
"region": "us-east-1",
"resources": ["arn:aws:cloudwatch:us-east-1:123456789012:alarm:prod-api-5xx"],
"detail": {
"alarmName": "prod-api-5xx",
"state": {
"value": "ALARM",
"reason": "Threshold Crossed: 1 out of the last 1 datapoints [42.0] was greater than the threshold (10.0).",
"timestamp": "2026-10-16T03:12:44.000+0000"
},
"previousState": { "value": "OK" },
"configuration": { "description": "API 5xx errors on the prod load balancer" }
}
}
Things worth knowing before you edit a template:
- The alarm lives under
detail.$.alarmNamematches nothing;$.detail.alarmNamedoes. state.reasonis generated text. It names the datapoint and threshold, which is exactly what you want to hear at 3 a.m.- Alarm names are inserted raw. A double quote inside an alarm name or reason would break the JSON the transformer produces. Stick to letters, digits, hyphens and underscores in alarm names — which is good practice anyway — or the call will fail with a 400.
- The link needs a plain name. A name with spaces or
#in it will not deep-link correctly; the console search still finds it.
Step 5 — Filter down to what deserves a call
An alarm channel that rings for every ALARM is the fastest way to teach yourself to ignore it. Filter in two places.
In the EventBridge pattern, which runs before anything is sent:
| Goal | Pattern change |
|---|---|
| Only production alarms | "alarmName": [{ "prefix": "prod-" }] inside detail |
| Skip anything tagged as non-urgent | Name them warn-… and use "alarmName": [{ "anything-but": { "prefix": "warn-" } }] |
| Only specific alarms | "alarmName": ["prod-api-5xx", "prod-db-cpu"] |
| Only real alarms, not test states | Keep "state": { "value": ["ALARM"] } — an INSUFFICIENT_DATA event never matches |
| One account or region only | Add "account": ["123456789012"] or "region": ["us-east-1"] at the top level |
In Echobell, use a channel condition for what the pattern cannot express, such as a time window:
state == "ALARM" && (hour >= 17 || hour < 9)
Echobell's time variables are in UTC, so convert from your own timezone first. The conditions reference lists every variable.
Step 6 — Give recoveries a quieter door
A second rule with "value": ["OK"] can tell you the problem has cleared, without ringing. Create a second Echobell channel of type Time Sensitive (or normal), a second API destination pointing at it, and reuse the same input transformer. The title template {{state}}: {{alarm}} already reads "OK: prod-api-5xx".
| Rule | State | Channel type | Behaviour |
|---|---|---|---|
alarms-to-call | ALARM, prod- prefix | Calling | Rings through Focus Mode |
alarms-warn | ALARM, warn- prefix | Time Sensitive | Lock screen, no ring |
alarms-recovered | OK | Normal | Ordinary push |
Test it before you trust it
Do not wait for production to break. Force an alarm into the state you want:
aws cloudwatch set-alarm-state \
--alarm-name prod-api-5xx \
--state-value ALARM \
--state-reason "Manual test of the Echobell path"
The alarm returns to its real state at its next evaluation, but the state change event is real and goes through your rule. To test only the Echobell side, trigger the channel directly:
curl -X POST https://hook.echobell.one/t/<channel-token> \
-H 'Content-Type: application/json' \
-d '{"alarm":"prod-api-5xx","state":"ALARM","reason":"Manual test","region":"us-east-1","link":"https://console.aws.amazon.com/cloudwatch/"}'
If the curl rings and the alarm test does not, the problem is in the rule, the transformer, or the destination — not the channel.
What about SNS and email-to-call?
If you already have an SNS topic everywhere and do not want to touch EventBridge, two other routes exist:
- SNS → Lambda → webhook. A few lines of code parse
Message, then POST a flat body to the same Echobell URL. It works, but you now own a function, a role, and its logs. - SNS → email → Echobell email trigger. Subscribe the channel's email trigger address to the topic. The alarm arrives as an email, and
{{subject}}and{{text}}in your templates become the notification. It is the least configuration of all, with the caveat that SNS email subscriptions need a one-time confirmation click and the text is not reshaped.
Subscribing the Echobell webhook URL directly to the topic as an HTTPS endpoint is the route to avoid: the confirmation request arrives as a notification, and every alarm after that is the long envelope described above.
Limits that matter during an incident
- 1 MiB body. Echobell rejects trigger bodies over 1 MiB with HTTP 413. A transformed alarm is a few hundred bytes, so you will not meet it here.
- 120 requests per minute per token. Past that the trigger answers
429withRATE_LIMIT_EXCEEDEDand aRetry-After. The destination's rate limit of 1 per second keeps you under it, and EventBridge retries events it could not deliver. - 1500 bytes of notification body. Longer renders are truncated.
state.reasonis a sentence or two and fits.
What this setup does not give you
- No acknowledgement. Answering the call tells CloudWatch nothing and does not silence other subscribers.
- No escalation or rotation. Everyone subscribed to the channel is called; nobody is called next.
- No deduplication beyond the alarm. A flapping alarm that moves
OK → ALARMevery few minutes will ring every time. Fix it with longer evaluation periods orDatapointsToAlarmon the alarm itself. - No two-way sync. Fixing the cause does not clear anything on your phone.
If those are dealbreakers, you want a full on-call platform. If what you need is "ring me when the production alarm fires", this is a handful of console screens.
Troubleshooting
The alarm fired but nothing arrived. Open the rule's Monitoring tab: Invocations shows whether the pattern matched, FailedInvocations whether delivery failed. Rules in a different region from the alarm never match.
The rule matches but the call fails with a 400. The transformer produced invalid JSON. Look for a quote or backslash in the alarm name or reason.
The notification is blank. The template is reading a field the transformer did not send. The body is flat: {{alarm}}, not {{detail.alarmName}}.
It rings repeatedly for one alarm. The alarm is flapping. Lengthen the evaluation period or require more datapoints; this is a CloudWatch setting, not an Echobell one.
It rings for an alarm you did not expect. Check the pattern for a missing prefix filter, and remember that composite alarms and alarms created by other tools (Auto Scaling policies, for example) emit state changes too.
Frequently asked questions
Can CloudWatch call a phone number natively?
No. CloudWatch alarm actions are SNS notifications, Auto Scaling, EC2 actions and Systems Manager. A voice call needs an external service: a Lambda that dials through a provider, an on-call platform, or a calling webhook like Echobell.
Do I need an SNS topic?
No. CloudWatch emits alarm state change events to EventBridge on its own, whether or not the alarm has any actions. You can leave the alarm's action list empty.
Will a CloudWatch alarm break through Do Not Disturb?
Only if it arrives as a call-style alert. A calling-type Echobell channel behaves like an incoming call, which iOS Focus Mode and Do Not Disturb let through. An SMS or an ordinary push does not.
Why not just subscribe the webhook URL to SNS?
SNS sends text/plain with the alarm serialized inside a Message string, so the template cannot read individual fields, and the envelope is long enough to be truncated. SNS also sends a subscription confirmation first, which would arrive as a notification.
Does this cost anything on AWS?
Alarm state change events reach the default event bus at no charge; API destination invocations are billed per request. At the volume of a healthy alarm set, that is a negligible amount, but check current EventBridge pricing for your region.
Can I call different people for different alarms?
Yes. Create one channel per audience, one rule per channel, and let each person subscribe with the notification type they want. There is no acknowledgement, so everyone subscribed with "calling" is called.
What about alarms in several regions or accounts?
Rules are per region, so create the rule in each region you use (the same API destination definition is repeated per region). For several accounts, send events to a central bus in one account and put the rule there.
Wrap-up
The path is five parts: a calling channel, a connection and API destination pointing at it, a rule that matches only the alarms worth waking someone for, an input transformer that flattens the event, and a forced set-alarm-state test before you rely on any of it. Everything else here is about keeping it narrow enough that a ring still means something a month from now.
Download Echobell for iPhone or get it on Google Play, then run the set-alarm-state test.