# SendOps Documentation
> SendOps is the observability and management layer for Amazon SES — analytics, Git-based template management, collaboration, and deliverability monitoring on top of your AWS SES account. This documentation covers setup, sending, templates, deliverability, billing, and AWS integration.
# What is SendOps?
Source: https://help.sendops.dev/getting-started/what-is-sendops
Markdown: https://help.sendops.dev/getting-started/what-is-sendops.md
Section: Getting Started
> An overview of SendOps — the observability and management layer for Amazon SES that gives your team visibility into every email you send.
SendOps is a web-based SaaS dashboard that sits on top of **Amazon Simple Email Service (SES)**. It gives your team real-time observability, GitHub-integrated template management, collaboration tools, and alerting — without changing a single line of your sending code.
Your application continues to send email through AWS SES using the AWS SDK you already have in place. SendOps **observes** the events SES produces and turns them into actionable insights. You never route mail through SendOps servers.
## Why SendOps?
AWS SES is a powerful, cost-effective email sending service — but it gives you very little visibility out of the box. Monitoring deliverability, tracking engagement, managing templates across environments, and collaborating as a team all require custom tooling that takes months to build and maintain.
SendOps fills that gap. Connect your AWS account, and within minutes you have:
- **Deliverability monitoring** — bounce rates, complaint rates, delivery metrics, ISP breakdown, and a reputation score at a glance.
- **Message-level tracing** — follow a single email from send to delivery, open, and click.
- **GitHub-integrated template management** — sync and manage your SES email templates from a connected GitHub repository.
- **Engagement analytics** — track opens, clicks, and template performance across your sending.
- **Multi-identity and multi-domain support** — manage multiple sending domains and email identities from a single dashboard.
- **Channel-based organization** — channels map to SES Configuration Sets, letting you organize sending by purpose (Transactional, Marketing, Onboarding, or Custom) with per-channel tracking and processing.
- **Team collaboration** — invite teammates and assign one of eight [roles](/team/members-and-roles) — from Owner and Org Admin through Developer, Marketer, and Financial to read-only Viewer — so the right people see the right data.
- **Webhook and Slack integrations** — connect external systems through the Connections section, including outbound webhooks and Slack notifications.
- **Audit logging** — track every significant action taken in your account for security and compliance.
- **Public API** — a REST API for messages, reports, suppressions, and configuration (read-only), plus write access to manage your contact roster and attribute definitions and to create and send broadcasts. See the [API reference](https://developers.sendops.dev) for the full catalogue.
## Who is SendOps for?
SendOps is built for three audiences that often work together but have different day-to-day needs:
| Audience | Role | What SendOps gives them |
|---|---|---|
| **Developers** | Build and maintain the application that sends email through SES | Message tracing, template management via GitHub, event logs, and a [public REST API](https://developers.sendops.dev) for programmatic access |
| **Marketers & Team Members** | Monitor campaign performance and email engagement | Dashboards, engagement metrics (opens, clicks), template performance tracking, deliverability reports, and scheduled notifications |
| **Infrastructure / AWS Admins** | Manage AWS accounts, IAM policies, DNS, and compliance | One-click CloudFormation setup, cross-account IAM role visibility, domain verification status, and audit logs |
## Key features
### Observability
Every SES event — sends, deliveries, bounces, complaints, opens, and clicks — is captured and stored in a high-performance analytics engine. Query millions of events in seconds. Deliverability monitoring includes ISP breakdown and a reputation score to help you stay ahead of issues.
### Template management
Sync your SES email templates from a connected GitHub repository. Track versions, preview renders, and manage templates across environments — all driven by your existing Git workflow.
### Channels
Organize your sending into **Channels** that map to SES Configuration Sets. Each channel has a Purpose type (Default, Transactional, Marketing, Onboarding, or Custom) and controls how emails are tracked and processed. Identities are assigned to channels for per-channel metrics and reporting.
### Team collaboration
Invite your team and assign a [role](/team/members-and-roles) — SendOps offers eight, including **Owner**, **Org Admin**, **Developer**, **Marketer**, and **Viewer** — so the right people see the right data. Developers get event logs and template controls; marketers get the audience and campaign tools; admins get infrastructure controls.
### Connections
Integrate SendOps with your existing tools through the Connections section. Set up outbound **webhooks** to push event data to external systems, or connect **Slack** to receive notifications in your team's channels.
### Notifications & alerts
Configure alerts for the metrics that matter — bounce rate thresholds, complaint spikes, domain verification failures, and more. Receive them in the dashboard, via email, or through your connected integrations.
## Plans
SendOps offers three tiers — **Free**, **Team**, and **Business** — so you can start at no cost and scale as your sending grows. Visit the [pricing page](https://sendops.dev/pricing) for current plan details and limits.
## What's next?
Ready to understand how the pieces fit together? Continue to [How SendOps Works](/getting-started/how-sendops-works) for a walkthrough of the architecture, or jump straight to [Creating Your Account](/getting-started/creating-your-account) to get started.
---
# How SendOps Works
Source: https://help.sendops.dev/getting-started/how-sendops-works
Markdown: https://help.sendops.dev/getting-started/how-sendops-works.md
Section: Getting Started
> Understand the architecture behind SendOps — how it connects to your AWS account, captures SES events, and turns them into actionable analytics.
This page is optional. You don't need to understand the architecture to use SendOps.
SendOps works by observing the events that Amazon SES already produces. It does not sit in the sending path for your application's own email — your application sends email exactly the way it does today, and SendOps captures what happens after. A few SendOps features go a step further and act on your SES account for you — sending Broadcasts and provisioning contact lists — always through your own SES, never through SendOps' mail servers.
Your application sends email through AWS SES using the AWS SDK, exactly as before, and SendOps observes the resulting events. SendOps connects to your AWS account via a cross-account IAM role, listens for SES events through EventBridge, and presents everything in the dashboard. Where a feature like Broadcasts does send mail, it does so through your own SES account using the access you grant during setup.
## The event flow
Here is the end-to-end path of an email event, from your application to the SendOps dashboard:
### Step by step
1. **Your application sends an email** using the AWS SDK (SES v2 API, SMTP, or any supported method). Nothing changes here.
2. **SES processes the email** and generates lifecycle events — `Send`, `Delivery`, `Bounce`, `Complaint`, `Open`, `Click`, and more — based on what happens with the message.
3. **A Configuration Set** is attached to your sending calls (or set as the account-level default). In SendOps, each Channel maps to an SES Configuration Set. The configuration set publishes events to Amazon EventBridge.
4. **An EventBridge rule in your AWS account** matches those events and forwards them to the SendOps webhook endpoint at `api.sendops.dev`.
5. **SendOps ingests the event**, enriches it with metadata (domain, channel, template), and writes it to a high-performance analytics engine built for fast aggregation over large volumes of event data.
6. **The SendOps dashboard** at `app.sendops.dev` queries the analytics engine in real time to power dashboards, reports, message tracing, and alerting.
## Connecting your AWS account
SendOps uses **cross-account IAM role assumption** to interact with your AWS account. This is the same pattern AWS recommends for granting third-party access — no long-lived API keys are stored.
During setup, you can connect your AWS account using one of two methods:
- **Automated (CloudFormation)** — a one-click stack deployment that creates all required resources.
- **Manual** — step-by-step IAM role creation for organizations that require manual provisioning.
Either method creates:
| Resource | Purpose |
|---|---|
| **IAM Role** | A role in your account that SendOps assumes. Scoped to the specific SES actions SendOps needs — mostly read-only, plus the write actions required by features like Broadcasts (bulk send) and contact-list provisioning. |
| **EventBridge Rule** | Routes SES events from the configuration set to the SendOps webhook. |
| **Webhook Target** | The EventBridge API destination that delivers events to `api.sendops.dev`. |
| **SES Configuration Set** | Captures event types and publishes them to EventBridge. |
The IAM role follows the principle of least privilege. SendOps only requests the permissions it needs — reading SES configuration, receiving events, and the specific write actions used by features you choose to use, such as sending Broadcasts and provisioning contact lists. It never sends your application's transactional mail on your behalf. See [IAM Permissions Explained](/aws-setup/iam-permissions) for the full policy breakdown.
### Why IAM roles instead of API keys?
- **No secrets to rotate.** There are no access keys stored in SendOps. The role is assumed on demand using AWS STS.
- **Revocable at any time.** Delete the CloudFormation stack and access is immediately revoked.
- **Auditable.** Every assumption is logged in AWS CloudTrail.
## Continuous AWS sync
Once connected, SendOps regularly syncs with your AWS SES account to discover new domains, email identities, and configuration sets. If something is added or removed in SES, SendOps detects it automatically and updates your dashboard. See [AWS Account Sync](/aws-setup/account-sync) for details.
## Templates
SendOps manages email templates through **GitHub integration**. Connect a GitHub repository, and SendOps syncs your templates automatically. Changes pushed to the repository are reflected in SendOps, keeping your templates version-controlled and aligned with your development workflow.
If you already have email templates in SES, SendOps can [export them as Git-ready files](/templates/importing-from-ses) so you can commit them to a repository and start managing them through SendOps right away.
## What SendOps stores
SendOps stores **SES event data** — delivery status, bounce details, complaint feedback, open/click tracking — in its analytics engine. It also stores metadata you configure in the dashboard: domains, channels, templates, team members, and notification rules.
SendOps does **not** store email content or message bodies. The events SES publishes contain headers, timestamps, and delivery metadata, but not the full message payload.
## Programmatic access
Everything the dashboard shows is also available through the [SendOps Public API](https://developers.sendops.dev) — a REST API at `api.sendops.dev` for messages, reports, suppressions, channels, templates, identities, and account state. Most resources are **read-only**, but you can also **write your contact roster** — create, update, delete, and bulk-import contacts, and manage their static-List membership — via the [Contacts API](https://developers.sendops.dev/api-reference/contacts), manage your [attribute definitions](https://developers.sendops.dev/api-reference/lists-segments#managing-attribute-definitions), and **create and send Broadcasts** programmatically. Authentication is via bearer API keys you create in your account settings. The API is server-to-server only (no browser-origin calls); where it does send — for example firing a Broadcast — the mail goes out through your own AWS SES account, never through SendOps' mail servers.
## Infrastructure at a glance
| Component | Technology | Role |
|---|---|---|
| Dashboard | `app.sendops.dev` | Web application — observability, templates, team management |
| API | `api.sendops.dev` | Receives webhook events, serves the dashboard |
| Analytics storage | SendOps Analytics Engine | High-speed event aggregation and querying |
| Event transport | Amazon EventBridge | Routes SES events from your account to SendOps |
| Auth to your AWS | IAM cross-account role | Secure, keyless access via STS |
## What's next?
Now that you understand how the pieces fit together:
- [Create your SendOps account](/getting-started/creating-your-account) to get started.
- [Connect your AWS account](/aws-setup/connecting-aws) to deploy the CloudFormation stack.
- [Learn about IAM permissions](/aws-setup/iam-permissions) to review exactly what access SendOps requests.
---
# Creating Your Account
Source: https://help.sendops.dev/getting-started/creating-your-account
Markdown: https://help.sendops.dev/getting-started/creating-your-account.md
Section: Getting Started
> Sign up for SendOps, explore the demo workspace, and complete the Setup Guide to connect your AWS account and start monitoring email.
Getting started with SendOps takes just a few minutes. Create an account, explore the demo workspace to familiarize yourself with the product, then complete the Setup Guide to connect your own AWS environment.
## Sign up
Open [app.sendops.dev](https://app.sendops.dev) in your browser. Click **Sign up** to begin.
You can create your account using:
- **GitHub** — sign in with your GitHub account.
- **Google** — sign in with your Google Workspace or personal Google account.
- **Email and password** — enter your full name, email address, and choose a password (minimum 8 characters).
Check the box to agree to the Terms of Service and Privacy Policy, then click **Create account**.
After creating your account, you are redirected to the sign-in page. Enter the credentials you just created and click **Sign in**.
## Demo workspace
After your first sign-in, you land in the **Demo Workspace** — a fully populated environment with sample data so you can explore the product before connecting your own AWS account. The demo includes example reports, identities, channels, and templates so you can see how everything fits together.
A banner at the top of the page reads **"Exploring with sample data."** When you are ready to set up your own workspace, click **Switch to your workspace**.
## The Setup Guide
When you switch to your own workspace, you land on the **Setup Guide** — a four-phase process that connects your AWS account and confirms everything is working end to end. You can always return to the Setup Guide from the sidebar.
Connect your AWS account to SendOps. Choose between two methods:
- **Automated Setup** Recommended — select your AWS region, then click **Launch CloudFormation Stack** to open the stack in your AWS console. The stack provisions the IAM role automatically. Once the stack is created, a link to the IAM role in CloudFormation appears to help you find and copy the Role ARN.
- **Manual Setup** — create the IAM role yourself using the policy document provided. The Setup Guide shows the full policy in both CloudFormation and Terraform formats with a copy button.
Both methods include:
- **AWS Region selector** — choose the region where your SES is configured.
- **Permission breakdown** — expandable categories showing exactly which permissions are requested: Account & Identities, Configuration Sets, Email Templates, Suppression List, Contact Lists, Resource Tagging, EventBridge, and Stack discovery. Each category shows the number of permissions it contains.
- **"Never accessed" badges** — a list of AWS services SendOps will never touch (EC2, S3, RDS, Lambda, Billing, IAM write, CloudWatch Logs) for transparency.
- **Role ARN field** — after creating the role (via either method), paste the Role ARN and click **Validate** to confirm permissions.
Click **"Delegate this step"** to send setup instructions to a teammate or AWS admin who has the required access. They will receive everything they need to complete the AWS Integration on your behalf.
A single AWS account can back more than one SendOps organization. If the account you enter is already connected to another organization, SendOps detects it and lets you **join the existing connection** instead of deploying a second CloudFormation stack.
The CloudFormation stack and its EventBridge rule are region-specific. If SendOps later finds your stack in a different AWS region than the one you selected, validation flags the mismatch — re-check the **AWS Region selector** so it matches the region where your SES sending (and stack) actually live.
See [AWS Integration](/aws-setup/connecting-aws) for a detailed walkthrough of both methods.
Verify that your CloudFormation stack deployed correctly and your SES configuration is active. Click **Run Validation** to check all components. The validation runs through 11 checks:
- **SES Configuration Set** — confirms the configuration set exists.
- **EventBridge Rule** — verifies the event rule is found.
- **Webhook Target** — checks the webhook endpoint is connected.
- **Channel Management** — validates configuration set management permissions.
- **Channel Settings** — confirms channel settings permissions.
- **Email Templates** — verifies email template management permissions.
- **Contact Lists** — verifies contact list access (`ses:ListContactLists`) so your SES contact list can sync.
- **SES enabled in region** — confirms SES is active in your selected region.
- **Account status** — reports whether you have production access or are in sandbox mode.
- **Sending limits** — displays your daily sending quota and max send rate.
- **Verified identities** — detects how many identities are already verified in your SES account.
Each check shows a green pass or red fail status. If your account is in SES sandbox mode, the **Account status** check passes with a yellow warning instead of green.
If any check fails, click **Retry Validation** after resolving the issue, or go **Back** to Phase 1 to adjust your configuration.
Add and verify your sending domains and email identities, and review the other SES resources SendOps picked up:
- **Domain Identities** — your verified sending domains. Columns: Identity and Verification status. Click **Add Domain** to register a new one. Each domain row is expandable to show DNS details and has per-row re-sync and delete actions.
- **Email Identities** — individual email addresses verified in SES. Columns: Identity and Verification status. Click **Add Email Identity** to verify a new address. Each row has re-sync and delete actions.
- **Configuration Sets** — SES configuration sets discovered in your account, which you can adopt as SendOps channels.
- **Contact Lists** — a read-only summary of the SES contact list SendOps imported, showing the total contacts and topics. Contact lists can be large, so individual contacts are not listed here and there is nothing to adopt — they import automatically.
If you already have identities in SES, click **Sync from AWS** (shown on first visit) or the **Re-sync** button to import them. You can also re-sync individual identities using the icon on each row.
The delete action on each row removes the identity from your AWS SES account, not just from SendOps. This is permanent and cannot be undone.
See [Adding a Domain](/domains/adding-a-domain) for detailed instructions on domain setup and DNS configuration.
Confirm the full pipeline is working by sending a test email. Enter a recipient address in the **Send test to** field and click **Send Test Email**.
Once delivered, you will see an **"Email delivered and confirmed"** success message showing the From and To addresses and a timestamp — confirming end-to-end delivery through your SES setup. You can click **Send another test** to verify additional addresses.
New SES accounts start in **sandbox mode**, which limits you to sending only to verified email addresses. You will need to request production access from AWS to send to any recipient. See [SES Sandbox & Production](/sending-email/ses-sandbox) for details.
## After setup
Once all four phases are complete, your dashboard begins populating with live email data. From here you can:
- **Explore the Messages dashboard** to trace individual emails and review delivery events.
- **Set up Channels** to organize your sending identities by purpose.
- **Invite team members** and assign roles so your colleagues can access the data they need.
- **Configure notifications** to get alerted when bounce rates spike or domains need attention.
## What's next?
If you have not yet connected your AWS account, head to [AWS Integration](/aws-setup/connecting-aws) to begin the setup process. For a closer look at the permissions SendOps requests, see [IAM Permissions Explained](/aws-setup/iam-permissions).
---
# Your agent already knows SendOps
Source: https://help.sendops.dev/agent-skill
Markdown: https://help.sendops.dev/agent-skill.md
Section: AI & Agents
> The SendOps agent skill turns Claude, Codex, Gemini, or any Agent Skills-compatible agent into a SendOps expert — guidance only, no account access. How to install it and what it can do.
The **SendOps agent skill** turns any agent you already use — Claude, Codex,
Gemini, or anything that supports the [Agent Skills](https://github.com/anthropics/skills)
standard — into a SendOps expert. Ask it "segment trial users who haven't opened
in 30 days" or "why aren't my opens being tracked," and it explains the concept,
drafts the exact thing you need, and tells you where to click.
It's **guidance only**. The skill never connects to your account, holds
credentials, or changes anything. It helps you *think, draft, and navigate* — you
stay in control of every action.
That's a separate, opt-in step: [connect an agent over MCP](/ai-agents/mcp-server).
The two complement each other — the skill teaches your agent how SendOps thinks,
the connection gives it live access to your account.
## What it can do
- **Explain** any SendOps concept in plain terms and how it maps to AWS SES underneath.
- **Draft** the exact artifact you need — a SendQL segment predicate, a List, an attribute schema, a template manifest snippet, the DNS records to paste.
- **Interpret** what a report or a status badge is telling you, and what to do next.
- **Navigate** — tell you which page, tab, or endpoint to use.
It covers the four areas of the product:
| Area | Examples |
| --- | --- |
| **Setup & onboarding** | connect AWS, verify a domain/DKIM, leave the SES sandbox, adopt an existing config set, fix "tracking off" |
| **Lists & Segments** | List vs Segment, writing SendQL predicates, the attribute registry, the null-handling footgun |
| **Reports & deliverability** | reading reports, suppression list vs undeliverable list, bounce/complaint hygiene |
| **Templates** | git-as-source-of-truth, the `sendops.json` manifest, Handlebars, per-org SES namespacing, test sends |
## What it doesn't do
The skill is deliberately honest about today's limits. It will tell you when
something isn't possible yet rather than pretend:
- **On its own, it does not touch your account.** It won't call the SendOps API or make changes for you. Live access is a separate, deliberate step — [connect the agent over MCP](/ai-agents/mcp-server) — and the skill knows the difference, so it tells you what it's about to do rather than implying it already did.
- The audience UI is **view-only** — it'll show you how to author definitions via the API or your connected GitHub repo.
- The Public API v1 is **read-mostly with a few write surfaces** — it can write your contact roster (create/update/delete and bulk import), attribute definitions, and **create and send broadcasts**, but not Lists or Segments. Template and workflow authoring is available over MCP only.
- There's no in-app template editor (SendOps is code-first) and no dedicated-IP management.
## Install it
The skill is a single Markdown file named `SKILL.md`. It installs the same way in
every agent: a folder named **`sendops`** containing that `SKILL.md`, dropped into
your agent's skills directory. Only the directory differs by agent.
The file must end up at `…/skills/sendops/SKILL.md` — a folder called `sendops`
with `SKILL.md` **inside** it. Don't rename the file itself to anything else.
### The one-liner (recommended)
From your SendOps dashboard, open **Connections › Integrations**, pick your agent
from the dropdown on the **SendOps agent skill** card, and copy the install
command — it creates the folder and downloads `SKILL.md` into the right place. Or
run it directly:
**Claude (Claude Code / Claude Desktop)**
```bash
mkdir -p ~/.claude/skills/sendops && \
curl -fsSL https://app.sendops.dev/sendops/SKILL.md -o ~/.claude/skills/sendops/SKILL.md
```
**Codex (OpenAI)**
```bash
mkdir -p ~/.codex/skills/sendops && \
curl -fsSL https://app.sendops.dev/sendops/SKILL.md -o ~/.codex/skills/sendops/SKILL.md
```
**Gemini CLI**
```bash
mkdir -p ~/.gemini/skills/sendops && \
curl -fsSL https://app.sendops.dev/sendops/SKILL.md -o ~/.gemini/skills/sendops/SKILL.md
```
Codex and Gemini also read a shared `~/.agents/skills/` directory if you'd
rather install once for both — use `~/.agents/skills/sendops/SKILL.md`.
### Or by hand
1. **Download `SKILL.md`** from **Connections › Integrations → Download SKILL.md**.
2. **Create a folder** called `sendops` inside your agent's skills directory
(`~/.claude/skills/`, `~/.codex/skills/`, or `~/.gemini/skills/`).
3. **Move `SKILL.md` into it**, so you end up with e.g. `~/.claude/skills/sendops/SKILL.md`.
4. **Reload skills** — most agents pick it up automatically; otherwise restart the
agent, or run `/skills reload` (Gemini) / `/skills` (Codex) to confirm it's listed.
That's it. The skill triggers automatically when you ask SendOps-related
questions — you don't have to invoke it by name.
### Verify it's working
Ask your agent something like *"In SendOps, what's the difference between the
suppression list and the undeliverable list?"* A correct answer (the suppression
list mirrors AWS SES; the undeliverable list is SendOps's own rule-based view)
means the skill is installed and triggering.
## Keeping it current
SendOps ships updates to the skill as the product evolves. When we publish a new
version, re-run the same one-liner (it overwrites `SKILL.md` in place) or
re-download from **Connections › Integrations**. The skill is versioned against
the product, so a fresh download always matches current behavior.
## Privacy & security
The skill is a single Markdown file of instructions — there's no code execution,
no network calls to SendOps, and no credentials involved. It only makes your agent
*better informed* about SendOps; your agent still does exactly what you ask, and
nothing reaches SendOps unless you separately connect the Public API or an MCP
server as a tool.
---
# Connect an AI agent to SendOps
Source: https://help.sendops.dev/ai-agents/mcp-server
Markdown: https://help.sendops.dev/ai-agents/mcp-server.md
Section: AI & Agents
> Give Claude, Cursor, or another AI agent live access to your SendOps organization over MCP — what it can do, how to connect it, and what stays off limits.
The [SendOps agent skill](/agent-skill) makes an agent *knowledgeable* about
SendOps. An **MCP connection** goes further: it lets the agent actually look at
your account and act on it.
*"Why did yesterday's campaign bounce so hard?"* — with the skill, your agent
explains how to find out. With an MCP connection, it looks, and tells you.
An agent connected over MCP can read your contacts and reports, and — depending
on what you approve — write contacts, author templates and workflows, and send
broadcasts to your audience. Connect agents you trust, and approve only the
permissions you need. You can review and disconnect anything you have connected
at any time from [Connected Apps](/api-integrations/connected-apps).
## What MCP is
MCP — the Model Context Protocol — is an open standard for connecting AI
assistants to outside systems. If your agent supports it, SendOps appears as a
set of **tools** the agent can use: look up a contact, size a segment, check
whether you can send, draft a broadcast.
You do not need to install anything on the SendOps side. There is one address:
```
https://mcp.sendops.dev/mcp
```
## What an agent can do
Thirty tools, grouped by what they act on. Your agent sees **only the ones your
approved permissions allow** — approve reads alone and the write tools are not
merely refused, they never appear.
| Area | What the agent can do |
| --- | --- |
| **Sending readiness** | Check whether your account can send at all, and what's blocking it — AWS connection, domain verification, the SES sandbox, tracking setup |
| **Deliverability** | Read your sending health, look through the suppression and undeliverable lists, search sent messages and diagnose why a particular one failed |
| **Audience** | Look up contacts, list your Lists and Segments, size a segment predicate *before* you save it, read your attribute and topic registry |
| **Audience writes** | Create and update contacts, archive them, change List membership, record activity |
| **Templates** | List templates, render one with test data, author or edit template content, send a test |
| **Drip Workflows** | Read workflow performance, dry-run a contact through one, author a workflow, estimate who it would enrol, activate it |
| **Broadcasts** | Read broadcast status, compose a draft, preview it, send a test, and send it for real |
One of the most useful things an agent can do is compile and count a segment
predicate without creating anything. Ask for "everyone who opened in the last
30 days but hasn't clicked", and it can tell you how many people that is before
you commit to a Segment.
## What it cannot do
- **It cannot bypass your permissions.** The agent acts *as you*. If your role
can't send broadcasts, neither can an agent connected under your account — even
if you approved that permission when you connected it. If your role is reduced
later, the agent's access shrinks with it, within a few minutes.
- **It cannot overwrite what your git repo owns.** If your Lists, topics, or
attributes are defined in a connected repository, the repo wins and the agent
is told so. For templates, an edit becomes a **pull request** for your team to
review, not a silent change.
- **It cannot send anything twice by accident.** Retries are recognised and
answered with the original result rather than repeated.
- **It cannot turn off your own safety checks.** There is no tool that disables a
workflow's send approval. An agent can ask to do something consequential; it
cannot remove the check on it.
## Sending and activating always ask first
Two actions can't be taken back: **sending a broadcast** and **activating a
workflow**. Both are deliberately made to take two steps. The agent's first
attempt doesn't send anything — it comes back with exactly what is about to
happen, and waits for an explicit confirmation before proceeding.
This is enforced by SendOps, not by your agent's own good manners. An agent that
ignored every warning still cannot send on the first call.
Everything reversible — updating a contact, saving a draft — happens in one step
on purpose. A confirmation prompt on every single action trains people to click
through them, which is exactly what would make the two that matter stop working.
## Connecting your agent
1. **Add the SendOps MCP server in your agent.** In Claude Desktop, Cursor, or
any MCP-capable client, add a connector and give it the URL
`https://mcp.sendops.dev/mcp`. The exact menu differs by app — look for
"Connectors", "MCP servers", or "Integrations".
2. **Sign in to SendOps.** Your agent opens a SendOps page in your browser. If
you're already signed in to the dashboard, you go straight to the next step.
3. **Review the consent screen.** It names the agent asking, lists every
permission it wants, and says which SendOps surface the access is for. Read
the list — this is the moment to decline anything that looks broader than what
you want the agent doing.
4. **Approve.** Your agent is connected. Ask it something — *"can my account send
email right now?"* is a good first test, since it needs only read access.
It won't work. The MCP server does not accept API keys — only the sign-in flow
above. API keys carry no user, no consent, and nothing to revoke per-agent,
which is not a credential that belongs on an agent connection. If your agent
only supports API keys, it can use the [Public
API](https://developers.sendops.dev) instead.
## Which organization it connects to
The connection is to **one organization** — the one you approved it for. If you
belong to several, connect the agent once per org; each gets its own approval and
can be revoked separately.
## Keeping an eye on it
Everything an agent does through MCP is attributed in your [audit
log](/team/audit-log), the same as any other change — you can see what was done,
and that it came through the connected app rather than from a person in the
dashboard. Reads aren't recorded there; the audit log tracks changes, not
lookups.
A connected agent is an authorized app like any other, so it appears in the same
two places and is revoked the same way:
- **[Connected Apps](/api-integrations/connected-apps)** (Profile → Connected
Apps) — the agents *you* connected, across every organization. Disconnect takes
effect immediately.
- **[OAuth Clients](/api-integrations/oauth-clients)** (Workspace → OAuth
Clients) — an Owner or Org Admin can review and revoke every grant across the
organization, which is the surface for off-boarding or an audit.
Whatever you approved, the connection is still capped by what *you* are allowed
to do. Reduce someone's role and their connected agents lose that access
automatically, within a few minutes — you don't have to revisit each one.
## For developers
Building a product that connects to SendOps on your customers' behalf? The
technical guide — the OAuth flow, client registration without an admin in the
loop, the tool schemas, guardrails, and rate limits — is at
[developers.sendops.dev/api-reference/mcp](https://developers.sendops.dev/api-reference/mcp).
---
# Reports Overview
Source: https://help.sendops.dev/reports/messages-dashboard
Markdown: https://help.sendops.dev/reports/messages-dashboard.md
Section: Reports & Analytics
> Navigate the SendOps Reports page to view messages, deliverability metrics, and engagement data across tabs with filtering, export, and auto-refresh.
Reports in SendOps is a single page with four tabs: **Messages**, **Deliverability**, **Engagement**, and **Undeliverable**. Each tab provides a focused view of your email activity. The first three share common controls for filtering; the [Undeliverable](/reports/undeliverable-list) tab has its own per-address search and status/reason filters.
## Shared controls
The following features are available across the **Messages**, **Deliverability**, and **Engagement** tabs:
- **Auto-refresh toggle** — enable automatic data refresh to keep the view up to date as new events arrive.
- **Channel filter** — use the dropdown to limit results to a specific [channel](/channels/understanding-channels).
- **Date range picker** — select a custom date range to focus on a specific period.
Exporting is available specifically from the **Event Log** (see below), which downloads the raw event stream as CSV or JSON.
## Messages tab
The Messages tab has two sub-tabs: **Messages** and **Event Log**.
### Messages sub-tab
The Messages sub-tab shows a list of individual messages processed through your connected AWS SES account. Each row includes:
| Column | Description |
|--------|-------------|
| **Status** | The current delivery status of the message (delivered, bounced, complained, etc.). |
| **Recipient** | The email address the message was sent to. |
| **Subject** | The subject line of the message. |
| **Timestamps** | When the message was sent and when key events occurred. |
Click any row to open the message detail view with the full event timeline, headers, and delivery information. For messages that were opened or clicked, the detail view also resolves the approximate location (country and city) of each open and click event.
### Event Log sub-tab
The Event Log sub-tab displays a raw stream of all SES events — deliveries, bounces, complaints, opens, and clicks — in chronological order. Use the Event Log to see exactly what happened and when, without the per-message grouping of the Messages view.
The Event Log supports filtering to narrow down to specific event types, making it useful for investigating delivery issues or tracking engagement patterns in real time. You can also **export** the current Event Log view — the filtered stream of events for your selected date range and channel — as a CSV or JSON file for offline analysis or sharing.
SendOps stores event data in a high-performance analytics engine designed for sub-second aggregation across billions of rows. Whether your account sends a hundred emails a day or a hundred thousand, queries return quickly.
## Deliverability tab
The Deliverability tab shows bounce rates, complaint rates, reputation metrics, and ISP-level breakdowns. For a detailed walkthrough of every metric on this tab, see [Deliverability Reports](/reports/deliverability-reports).
## Engagement tab
The Engagement tab tracks open rates and click rates over time, with breakdowns by domain and template. For a detailed walkthrough, see [Engagement Metrics](/reports/engagement-metrics).
## What's next?
- Review [Deliverability Reports](/reports/deliverability-reports) to monitor bounce and complaint rates.
- Check [Engagement Metrics](/reports/engagement-metrics) for open and click tracking data.
- If you notice high bounce rates, see [Deliverability Problems](/troubleshooting/deliverability-problems) for troubleshooting steps.
---
# Deliverability Reports
Source: https://help.sendops.dev/reports/deliverability-reports
Markdown: https://help.sendops.dev/reports/deliverability-reports.md
Section: Reports & Analytics
> Track bounce rates, complaint rates, SES account status, per-provider deliverability with trend deltas, delivery latency, and suppression data in the SendOps Deliverability tab.
The Deliverability tab on the [Reports](/reports/messages-dashboard) page helps you understand whether your email is reaching inboxes. SendOps tracks every bounce and complaint event from AWS SES and surfaces key metrics, per-provider deliverability with trend deltas, latency data, and suppression information.
## Headline metrics
The top of the Deliverability tab displays the following metrics for the selected date range:
| Metric | Description |
|--------|-------------|
| **Hard Bounce %** | The percentage of sent messages that resulted in a hard bounce (permanent delivery failure). |
| **Soft Bounce %** | The percentage of sent messages that resulted in a soft bounce (temporary delivery failure). |
| **Total Bounce %** | The combined bounce rate (hard + soft bounces). |
| **Complaint Rate** | The percentage of sent messages that generated a spam complaint. |
AWS monitors your sending reputation continuously. If your bounce rate exceeds **5%** or your complaint rate exceeds **0.1%**, SES may place your account under review, limit your sending, or suspend it entirely. SendOps displays these thresholds as reference lines so you can see how close you are at a glance.
## SES Account Status
Alongside the headline rates, the Deliverability tab surfaces the current status of your AWS SES account, read live from SES. This tells you at a glance whether AWS itself will let your mail go out:
| Indicator | What it tells you |
|-----------|-------------------|
| **Sending enabled** | Whether SES is currently allowing your account to send at all. If sending is disabled, no mail will leave regardless of your reputation. |
| **Sandbox vs. production** | Whether your account is still in the SES sandbox (can only send to verified addresses) or has been granted production access. See [SES Sandbox & Production Access](/sending-email/ses-sandbox). |
| **Enforcement status** | AWS's current enforcement state for the account — `HEALTHY` when there are no issues, or a warning/review state when AWS has flagged your sending. |
| **VDM (Virtual Deliverability Manager)** | Whether AWS's Virtual Deliverability Manager is enabled on the account, which unlocks additional SES deliverability insights. |
## Bounce Reasons
A bar chart shows categorized bounce reasons, breaking down why messages bounced. This helps you distinguish between list quality issues (invalid addresses), mailbox-full conditions, policy rejections, and other bounce categories so you can take targeted corrective action.
## Complaint Reasons
A breakdown of complaint reasons with percentages, showing why recipients reported your messages as spam. Use this to identify patterns — for example, a high percentage of "moved to junk" complaints may indicate content or frequency issues.
## Deliverability by Provider
The Deliverability by Provider table shows per-provider sending volume, deliverability rates, and how those rates have shifted versus the equal-length period immediately before the one you're viewing.
The top eight providers by current-period sends are listed individually, with the long tail aggregated under **Other**. The provider list is derived from your actual sending data — your top destinations show up automatically, whether they're US providers (Gmail, Microsoft, Yahoo, Apple), European providers (Free.fr, Orange, GMX, Web.de, Yandex, Mail.ru, Libero, Seznam, etc.), or APAC providers (Naver, QQ, NetEase, Sina, Daum, Rediff, etc.). SendOps recognises common alias domains (gmail.com + googlemail.com → Gmail; outlook.com + hotmail.com + live.com + msn.com + regional TLDs → Microsoft; Yahoo regional TLDs → Yahoo; etc.) so they collapse under one canonical name. Domains SendOps doesn't recognise appear under the raw recipient domain, so nothing is hidden from view.
| Column | Description |
|--------|-------------|
| **Provider** | The receiving provider, derived from the recipient email domain. |
| **Sends** | Total messages sent to recipients at this provider. |
| **Deliveries** | Messages confirmed delivered. |
| **Bounces** | Messages that bounced. |
| **Bounce %** | Bounces as a percentage of Sends. The annotation underneath shows the change in percentage points versus the prior period. |
| **Complaints** | Messages that generated spam complaints. |
| **Complaint %** | Complaints as a percentage of Sends, with the prior-period delta underneath. |
| **Rejects** | Messages that AWS SES refused to send (typically due to content rejection or recipient suppression). |
| **Reject %** | Rejects as a percentage of attempted messages (Sends + Rejects, since rejected messages are not counted in Sends), with the prior-period delta underneath. |
Click any column header to sort by that column. **Other** always appears at the bottom regardless of sort, since it is a residual rather than a ranked provider.
The trend deltas (the small `+0.40pp` / `-0.05pp` annotations under each rate) are the primary signal for spotting deliverability issues: a provider whose bounce or complaint rate has crept up versus the prior period is the one to investigate, even if the absolute rate is still under the SES thresholds. Positive deltas (rates getting worse) appear in red; negative deltas (rates improving) appear in green. Brand-new providers with no prior-period activity show no delta, since there's nothing to compare against.
## Delivery Latency
The Delivery Latency section shows how long it takes for messages to be accepted by recipient mail servers. Three percentile metrics are displayed:
| Metric | Description |
|--------|-------------|
| **P50** | The median delivery latency — half of messages are delivered faster than this. |
| **P95** | 95% of messages are delivered within this time. |
| **P99** | 99% of messages are delivered within this time. Only the slowest 1% take longer. |
A chart visualizes latency trends over the selected date range, making it easy to spot degradation or improvement over time.
## Suppression List
The Suppression List viewer shows email addresses that have been suppressed. Suppressed addresses are those that SES will not attempt to deliver to, typically due to previous hard bounces or complaints. Use this section to review which addresses are suppressed and understand the reasons.
If you've confirmed an address is safe to mail again — for example, a recipient has fixed their mailbox — you can **remove it from the suppression list** directly from this viewer. SES will then attempt delivery to that address on future sends. Removing an address requires the `suppression.manage` permission; by default this is granted to **Owner** and **Org Admin** roles.
This SES suppression list is distinct from SendOps's own [Undeliverable List](/reports/undeliverable-list), which layers additional configurable rules on top of the raw AWS signals.
## Improving deliverability
If your rates are trending in the wrong direction, here are the most effective steps:
1. **Clean your recipient list.** Remove addresses that have hard-bounced. Implement double opt-in for new subscribers to ensure valid addresses.
2. **Monitor complaint sources.** Check which campaigns or message types are generating complaints. A high complaint rate on a specific channel or template is a signal to revisit the content or frequency.
3. **Use a dedicated domain for transactional email.** Separate transactional email (order confirmations, password resets) from marketing email by using different domains or subdomains. This isolates reputation issues.
4. **Authenticate your email properly.** Ensure SPF, DKIM, and DMARC are correctly configured. See [DNS Configuration](/domains/dns-configuration) for setup details.
5. **Warm up new domains gradually.** If you add a new sending domain, start with low volume and increase over days or weeks. Sudden high-volume sending from an unknown domain triggers ISP suspicion.
For a deeper walkthrough of common deliverability issues and how to resolve them, see [Deliverability Problems](/troubleshooting/deliverability-problems).
## What's next?
- Review the [Reports Overview](/reports/messages-dashboard) to investigate individual bounced or complained messages.
- Check [Engagement Metrics](/reports/engagement-metrics) to see how recipients interact with your delivered email.
- Set up [Notifications](/notifications/configuring-notifications) to get alerted when bounce or complaint rates cross a threshold.
---
# Engagement Metrics
Source: https://help.sendops.dev/reports/engagement-metrics
Markdown: https://help.sendops.dev/reports/engagement-metrics.md
Section: Reports & Analytics
> Track open rates, click rates, and recipient engagement with time-series charts, domain breakdowns, and template performance tables in SendOps.
The Engagement tab on the [Reports](/reports/messages-dashboard) page shows you what happens after your email lands in the inbox. SendOps tracks opens and clicks for every message that passes through AWS SES and presents the data through dedicated charts and tables.
## Open Rate Over Time
A time-series chart showing open rates across the selected date range. Use this to spot trends in recipient engagement — a declining open rate may indicate list fatigue or deliverability issues pushing messages to spam.
## Click Rate Over Time
A time-series chart showing click rates over the same period. This tracks how often recipients click links in your emails, giving you a measure of content effectiveness.
## Engagement by Domain
A bar chart breaking down engagement metrics by recipient domain. This shows which domains (e.g., gmail.com, outlook.com, yahoo.com) have the highest and lowest engagement, helping you identify domain-specific issues or patterns.
## Engagement by Template
A bar chart showing engagement broken down by template. Compare how different templates perform to identify high-performing content and templates that may need revision.
## Template Performance Breakdown
A detailed table listing every template with its key performance metrics:
| Column | Description |
|--------|-------------|
| **Template** | The template name. |
| **Sent** | Total messages sent using this template. |
| **Open%** | The percentage of sent messages that were opened. |
| **Click%** | The percentage of sent messages where at least one link was clicked. |
| **Bounce%** | The percentage of sent messages that bounced. |
| **Complaint%** | The percentage of sent messages that generated a spam complaint. |
This table gives you a single view of how each template performs across all key metrics, making it easy to compare templates and identify ones that need attention.
## How tracking works
SES provides open and click tracking natively. When tracking is enabled on your configuration set, SES modifies outgoing messages before delivery:
### Open tracking
SES inserts a tiny, invisible **tracking pixel** (a 1x1 transparent image) at the end of the HTML body. When the recipient's email client loads images, it requests the pixel from SES, which records an `Open` event. That event flows through EventBridge to SendOps just like any other SES event.
### Click tracking
SES rewrites links in the HTML body to point to a **redirect URL**. When the recipient clicks a link, the request goes to SES first, which records a `Click` event with the original destination URL, then redirects the recipient to the intended page. The redirect is fast and transparent to the recipient.
If you have configured a [tracking domain](/domains/tracking-domains), SES uses your custom domain for both the tracking pixel and redirect URLs instead of the default SES domain. This improves deliverability and provides a more professional appearance.
Open tracking requires the message to be **HTML email** — plain-text messages have no image support. Additionally, many email clients block external images by default or use privacy proxies (such as Apple Mail Privacy Protection) that pre-fetch images regardless of whether the recipient actually reads the message. Treat open rates as a directional signal rather than an exact count.
## Reading the data
A few patterns to watch for:
- **High open rate, low click rate** — recipients are opening but not engaging with the content. Consider whether calls to action are clear and above the fold.
- **Declining open rate over time** — may indicate list fatigue. Recipients are losing interest or your messages are being filtered to spam. Check your [deliverability reports](/reports/deliverability-reports) for rising complaint rates.
- **Sudden drop in open rate** — could reflect a deliverability issue (messages going to spam) rather than a content issue. Cross-reference with bounce and complaint trends.
## What's next?
- Set up a [Tracking Domain](/domains/tracking-domains) so tracking URLs use your own domain instead of the default SES domain.
- Review the [Reports Overview](/reports/messages-dashboard) to see open and click events on individual messages.
- Check [Deliverability Reports](/reports/deliverability-reports) to ensure your messages are reaching the inbox in the first place.
---
# Undeliverable List
Source: https://help.sendops.dev/reports/undeliverable-list
Markdown: https://help.sendops.dev/reports/undeliverable-list.md
Section: Reports & Analytics
> View permanent-failure addresses (bounces, complaints, rejects) and configurable rule-based detections across your account, search them, and clear individual addresses so SendOps allows delivery to them again.
The **Undeliverable** page lists every recipient address that has produced an undeliverable signal in roughly the last year — permanent bounces, complaints, rejects, plus the configurable rules you have enabled (repeated transient bounces, undetermined bounces, and soft-bounce accumulation) — along with any addresses you have explicitly cleared. It complements the [Deliverability Reports](/reports/deliverability-reports) by giving you a per-address operational view rather than aggregate rates.
## What gets listed
An address appears on the Undeliverable list when SendOps has ingested events that match an enabled classification rule. Three rules are always-on; three are configurable.
### Always-on rules
| Event | Condition | Reason |
|-------|-----------|--------|
| Bounce | `bounceType = Permanent` (any sub-type — `NoEmail`, `General`, `Suppressed`, `OnAccountSuppressionList`, etc.) | `permanent_bounce` |
| Complaint | Any complaint | `complaint` |
| Reject | Any reject (content refused before sending) | `rejected` |
These three rules cannot be disabled. Complaints are locked on for compliance reasons (CAN-SPAM, CASL, GDPR); permanent bounces and rejects are the AWS-confirmed signals at the core of any suppression discipline.
### Configurable rules
| Rule | What it catches | Default |
|------|-----------------|---------|
| `repeated_transient` | Receivers that accept the message then asynchronously reject it (Mimecast / Office 365 / Proofpoint accept-then-NDR), surfaced as `Transient/General` bounces in SES | On — 3 events in 30 days |
| `undetermined` | SES couldn't classify the bounce (rare, ~1% of events) | Off — 2 events in 14 days |
| `soft_bounce_accumulation` | Transient bounces with sub-type `MailboxFull`, `MessageTooLarge`, `ContentRejected`, or `AttachmentRejected` | Off — 5 events in 14 days |
Each configurable rule has two knobs: **events** (how many qualifying events must occur) and **window** (how many days back to look). When ≥ N qualifying events have happened for an address in a rolling M-day window ending now, the rule fires. Tune both from the [Classification Rules](/reports/classification-rules) page.
Successful events (`Send`, `Delivery`, `Open`, `Click`) never put an address on this list. `DeliveryDelay` never qualifies on its own either — only bounces, complaints, and rejects do.
This list is different from the AWS SES suppression list. AWS only auto-suppresses permanent/General bounces and complaints, and entries there expire after 14 days by default. SendOps's Undeliverable list includes a wider set of signals (and your configurable rules), with no automatic expiry — so it tends to be broader and longer-lived.
## Reading a row
Each row shows the address along with:
| Column | What it tells you |
|--------|-------------------|
| **Email** | The recipient address. |
| **Status** | `Listed` — currently failing delivery. `Excluded` — you've cleared this address and SendOps will allow delivery to it again. |
| **Reason** | The rule that matched this address. When multiple rules match, the highest-priority one wins: `permanent_bounce` > `complaint` > `rejected` > `repeated_transient` > `undetermined` > `soft_bounce_accumulation`. For permanent bounces and complaints, a sub-type is appended (e.g. `permanent bounce · NoEmail`, `complaint · abuse`). |
| **Events** | How many qualifying events SendOps has seen for this address in the account's ~365-day event window. Useful for triage — an address with 1,698 hits jumps out instantly. |
| **Last change** | When the row last changed state. For listed rows this is the most recent failure event; for excluded rows it's when you cleared the address. |
| **Last diagnostic** | The SMTP response from the most recent event (e.g. `550 5.1.1 : Recipient address rejected: User unknown`). Distinguishes "user unknown" from "domain offline" from "policy refused", which matters when you go talk to a customer. For excluded rows this column shows the operator note instead, if one was recorded. |
## Filtering and search
Three controls narrow what you see:
- **Search email** — case-insensitive substring match against the recipient address.
- **Status** — `Listed` (default), `Excluded` (just your cleared addresses), or `All` (both).
- **Reason** — restrict listed rows to any of the six reason values.
Filters compose. Searching `acme` with `Status: All` will show every `@acme.com` address regardless of whether you have cleared it.
## Allowing delivery to an address
If you're confident an address is fine — a customer typo you've corrected on their end, a fixed inbox after IT work, or a false positive — you can clear the address from the list. SendOps will allow delivery to it again from that point onward.
1. Find the address in the list (use search if needed).
2. Click **Allow** on the row.
3. Optionally add a short note (e.g. "Customer fixed mailbox after IT migration"). Notes are recorded in the audit log alongside the operator who took the action.
4. Confirm.
The row immediately moves to `Excluded` and disappears from the default view. You can see it again by switching the status filter to `Excluded` or `All`.
Allowing delivery to an address that is genuinely undeliverable will likely cause it to bounce again. If that happens, SendOps will automatically put the address back on the list with `Listed` status — your clearing decision is overridden by the new failure. There is no penalty for the round-trip, but for high-volume addresses you may want to verify the underlying issue is fixed before allowing.
## Re-listing a cleared address
If you change your mind, switch the status filter to `Excluded`, find the address, and click **Re-list**. The exclusion record is removed; the address returns to the undeliverable view if any qualifying events still exist within the account's ~365-day event window.
## Configuring which rules apply
Click **Configure rules** in the top-right of the page (or visit `/undeliverable/rules`) to choose between the **Strict** / **Standard** / **Aggressive** preset profiles or to set individual rule thresholds. See [Classification Rules](/reports/classification-rules) for the full configuration guide.
A live preview on that page shows how the list size and breakdown would change before you save — useful when you're considering turning on a new rule.
## Permissions
| Action | Required permission |
|--------|---------------------|
| View the Undeliverable page | `undeliverable.view` |
| Allow delivery to an address (clear it) | `undeliverable.manage` |
| Re-list a cleared address | `undeliverable.manage` |
| Change classification rules | `undeliverable.configure` |
By default, **Owner** and **Org Admin** roles have all three permissions. **Member** and **Support** roles can view but not modify. Operators without a permission will see the action buttons disabled with an explanatory tooltip.
Every clear / re-list / rules-change action is recorded in the [Audit Log](/team/audit-log) with the operator's identity, before/after snapshots where relevant, and any note they added.
## How far back the list looks
The listed portion of the page is bounded by a fixed window of roughly the **last 365 days** — an address whose most recent failure was over a year ago falls off the list. This window is the same for every account; it is not tied to your plan's retention setting. (Plan retention *does* apply to the programmatic `GET /v1/undeliverable` API, but not to this dashboard.) Cleared addresses (excluded rows) live in durable storage and survive regardless of age; they remain visible under `Status: Excluded` no matter how long ago you cleared them.
For the windowed rules (`repeated_transient`, `undetermined`, `soft_bounce_accumulation`), the lookback is instead the rule's own **window** knob — for example, with `repeated_transient` set to 3 events in 30 days, only events within the last 30 days count toward the threshold. Older events don't keep an address on the list past the window; the row falls off automatically once recent events no longer satisfy the rule. Cleared exclusions are unaffected.
## What's next?
- Tune which signals make an address undeliverable on the [Classification Rules](/reports/classification-rules) page.
- Use [Deliverability Reports](/reports/deliverability-reports) to see aggregate bounce and complaint rates over time.
- Configure [Notifications](/notifications/configuring-notifications) to be alerted when your bounce or complaint rates cross a threshold.
- If you're seeing many undeliverable addresses from a single provider, the [Deliverability by Provider](/reports/deliverability-reports#deliverability-by-provider) table can help identify the source.
---
# Classification Rules
Source: https://help.sendops.dev/reports/classification-rules
Markdown: https://help.sendops.dev/reports/classification-rules.md
Section: Reports & Analytics
> Tune which bounce, complaint, and rejection signals put an address on your undeliverable list. Choose a Strict / Standard / Aggressive profile or configure each rule individually, with a live preview of the impact before you save.
The **Classification Rules** page controls which signals put an address on your [Undeliverable List](/reports/undeliverable-list). Different operators have legitimately different tolerance for false positives — a bank cares about every legitimate send, a marketing team cares more about list hygiene — so the rules are configurable per organization.
You can reach this page from the **Configure rules** button in the top-right of the Undeliverable list, or directly at `/undeliverable/rules`.
## Profiles
Pick a profile to apply the recommended defaults. The "Custom" indicator appears automatically when your individual rule settings don't match any preset.
| Profile | What it includes | Typical fit |
|---------|------------------|-------------|
| **Strict** | Locked rules only — permanent bounces, complaints, rejects | Banks, strictly transactional senders. Lowest false-positive risk. |
| **Standard** *(default)* | Locked rules + repeated transient bounces (3 events in 30 days) | Most orgs. Catches Mimecast / Office 365 / Proofpoint accept-then-NDR patterns. |
| **Aggressive** | Standard + undetermined (2 in 14 days) + soft-bounce accumulation (5 in 14 days) | Marketing or bulk senders where bounce-rate reputation is existential. |
| **Custom** | Whatever you've configured in the Advanced section | Auto-selected when knobs deviate from a preset. |
Picking a named profile resets every configurable rule to that profile's defaults. If you have unsaved changes, SendOps will confirm before discarding them.
## Always-on (locked) rules
Three rules cannot be disabled. They are the core compliance and correctness signals.
- **Permanent bounces** (any sub-type — `NoEmail`, `General`, `Suppressed`, `OnAccountSuppressionList`, etc.) — the AWS-confirmed signal that an address is dead.
- **Complaints** — required for compliance (CAN-SPAM, CASL, GDPR). Disabling these would be a legal footgun.
- **Rejects** — SES already refused the send. Suppressing is correctness, not policy.
These appear in the Advanced section as read-only with a lock icon.
## Configurable rules
Each configurable rule has two knobs:
- **Events** — how many qualifying events must occur (integer, 1–100)
- **Within (days)** — how many days back to look (integer, 1–365)
When at least that many qualifying events have happened for an address inside the **Within (days)** window ending right now, the rule fires.
### Repeated transient bounces
Catches the receiving MTA returning `250 OK` to SES at SMTP time, then asynchronously generating a Non-Delivery Report. SES emits this as `Bounce / Transient / General` even though the address is effectively permanently undeliverable. Mimecast, Office 365, and Proofpoint all behave this way.
- **Counts**: Transient bounces with sub-type `General` only
- **Default**: 3 events in 30 days, on under Standard
This is the rule most likely to recover undelivered addresses from your real traffic. For one early customer it surfaced 116 missing addresses that were generating thousands of bounces but never appearing on the Strict-only list.
### Undetermined bounces
SES couldn't classify the bounce. Rare (~1% of events) but real — and sometimes the only signal you get for a domain that intermittently refuses mail.
- **Counts**: Bounces with `bounceType = Undetermined`
- **Default**: 2 events in 14 days, off
Turning this on cleans the long tail at the cost of occasional false positives where a transient infrastructure problem produced the events.
### Soft-bounce accumulation
For mailboxes that keep failing for the same reason — quotas, message size, content rejection. The address isn't dead, but continuing to send is wasted budget and bad reputation.
- **Counts**: Transient bounces with sub-type `MailboxFull`, `MessageTooLarge`, `ContentRejected`, or `AttachmentRejected`
- **Default**: 5 events in 14 days, off
`ChannelLimitExceeded` is intentionally excluded — that's a sender-side capacity issue, not a recipient signal.
## Live preview
As you adjust knobs, the **Live preview** panel re-fires within ~300ms and shows:
- **Current list size** — how many addresses are on the list right now under your active rules
- **With these rules** — how many would be on the list with the candidate rules, with a `+added, −removed` delta
- **Breakdown** — by-reason counts for the candidate list
- **Show sample additions** — up to 10 specific addresses that would be added, with their event counts, so you can sanity-check that the rule change catches the right kind of failures
The preview is read-only — nothing is saved until you click **Save rules**. If a preview call fails (the panel will say "Computing…" indefinitely), the saved rules are unaffected.
## Reason priority
An address often matches multiple rules. The wire `reason` field on the Undeliverable list (and the `/v1/undeliverable` API response) is the highest-priority match, in this order:
1. `permanent_bounce`
2. `complaint`
3. `rejected`
4. `repeated_transient`
5. `undetermined`
6. `soft_bounce_accumulation`
So if an address has both a permanent bounce and 5 repeated transient bounces, it appears with `reason: permanent_bounce` — not both.
## Saving and the audit trail
**Save rules** persists the rule body and stamps a short version hash. Every change is captured in the [Audit Log](/team/audit-log) with before/after snapshots of the rule and the operator who made the change.
Each saved rule set carries a version identifier, which is also exposed to the API so programmatic consumers can tell when your rules have changed and re-sync accordingly. See the developer API reference for details.
## Removing an address from the list
Changing rules controls **which** addresses are detected. To remove a specific address (without changing the rules for everyone), use the **Allow** action on the [Undeliverable List](/reports/undeliverable-list#allowing-delivery-to-an-address). Allowed addresses are recorded as exclusions and override the rule outcome until a new failure event comes in.
## Permissions
| Action | Required permission |
|--------|---------------------|
| View the Classification Rules page | `undeliverable.view` |
| Save changes | `undeliverable.configure` |
By default only **Owner** and **Org Admin** roles can change rules — changing classification policy affects every API caller, every team member's view of the list, and every notification rule that's keyed off undeliverable counts. Other roles see the page in view-only mode with a banner.
The **Allow / Re-list** actions on the Undeliverable list itself use a separate permission (`undeliverable.manage`), so a member-level operator can clear individual addresses without being trusted to change the org-wide rule set.
## What's next?
- See [Undeliverable List](/reports/undeliverable-list) for the day-to-day workflow of triaging individual addresses.
- The [Deliverability Reports](/reports/deliverability-reports) show aggregate bounce and complaint rates — useful for picking rule thresholds based on real volumes.
- For programmatic access to the rules (read-only), see `GET /v1/undeliverable/rules` in the API reference.
---
# Configuring Notifications
Source: https://help.sendops.dev/notifications/configuring-notifications
Markdown: https://help.sendops.dev/notifications/configuring-notifications.md
Section: Notifications & Alerts
> Manage your notification inbox, set delivery preferences, use presets, and configure team-wide notification settings in SendOps.
The SendOps notification system is organized into four tabs: **Inbox**, **Preferences**, **Presets**, and **Team**. Each tab serves a different purpose in managing how you and your team receive alerts.
## Inbox
The Inbox tab displays a chronological list of your notifications with read and unread status indicators. From here you can:
- **View notifications** — each notification shows the event details, timestamp, and source.
- **Mark as read or unread** — click a notification to mark it as read, or use bulk actions to manage multiple notifications at once.
- **Navigate to source** — click a notification to jump directly to the relevant page in the dashboard (e.g., a deliverability alert takes you to the affected domain's metrics).
## Preferences
The Preferences tab lets you control which notification types you receive and how they are delivered. Each notification type has toggles for the available delivery channels.
Navigate to **Notifications → Preferences** in the SendOps dashboard.
Each notification type has toggles for the delivery channels available to you. Enable or disable each combination to control exactly which alerts you receive and how. Changes are saved automatically.
A small set of notification types are **enforced**: you can choose *how* you receive them, but you can't turn off every channel at once — at least one delivery channel always stays on. This covers **billing** events (payment failures, overdue invoices, plan limits, etc.) and **team access** changes (a member being invited or a role being changed), so these business- and security-critical alerts can't go completely unseen. All other notification types, including SES health and deliverability alerts, can be fully disabled if you choose.
## Presets
The Presets tab provides reusable notification preference templates. Presets let you save a specific combination of notification preferences and apply them quickly — useful for standardizing notification settings across your team or switching between different configurations.
## Team
The Team tab is only visible to Owners and Admins.
The Team tab lets administrators view and manage notification settings for all workspace members. From here you can:
- See each team member's current notification preference configuration.
- Adjust notification settings on behalf of team members.
- Ensure that critical alerts are properly configured across the team.
## Webhooks and integrations
Webhook configuration is not part of the Notifications section. To set up webhooks for receiving SendOps events programmatically, navigate to **Connections → Webhooks** in the product sidebar. See [Webhooks](/notifications/webhooks) for full setup details.
Similarly, Slack and other third-party integrations are configured under **Connections → Integrations** in the product sidebar.
## What's next?
- Review the full list of available alerts in [Notification Types](/notifications/notification-types).
- Set up programmatic event handling with [Webhooks](/notifications/webhooks).
- Manage who has access to your workspace in [Team Members & Roles](/team/members-and-roles).
---
# Notification Types
Source: https://help.sendops.dev/notifications/notification-types
Markdown: https://help.sendops.dev/notifications/notification-types.md
Section: Notifications & Alerts
> A complete reference of every notification SendOps can send, organized by category — from deliverability alerts and SES health warnings to billing events and team activity.
SendOps supports more than 50 notification types across nine categories. Each notification can be delivered through any combination of channels you configure — in-app, email, Slack, or outbound [webhooks](/notifications/webhooks).
This page is a complete reference. If you are looking for how to turn notifications on or off, see [Configuring Notifications](/notifications/configuring-notifications).
A handful of account-lifecycle milestones — `channel_events.domain_verified`, `system_infrastructure.aws_connected`, `ses_account_health.first_send`, and `ses_account_health.production_access_granted` — are **enabled by default**, so integrations can react to key setup events (domain verified, AWS connected, first email sent, production access granted) without any extra configuration.
## Notification Presets
SendOps provides **Notification Presets** — reusable templates for notification preferences that you can apply to quickly configure notification settings. Instead of toggling individual notifications one by one, select a preset that matches your needs and apply it. You can also create custom presets tailored to your workflow.
## Team notification management
Admins can view and manage notification preferences for all team members from the **Team** tab in the Notifications section. This gives administrators a centralized view of who is receiving which notifications, and the ability to adjust preferences on behalf of team members — useful for ensuring critical alerts reach the right people.
## Deliverability
Deliverability notifications fire when your sending reputation metrics cross thresholds or show concerning trends.
| Type | Key | Description |
|------|-----|-------------|
| Bounce rate threshold | `deliverability.bounce_rate_threshold` | The bounce rate for a domain or channel has exceeded the configured threshold. |
| Complaint rate threshold | `deliverability.complaint_rate_threshold` | The complaint (spam report) rate has exceeded the configured threshold. |
| Bounce rate trend | `deliverability.bounce_rate_trend` | An upward trend in bounce rates has been detected over the evaluation window. |
| Complaint rate trend | `deliverability.complaint_rate_trend` | An upward trend in complaint rates has been detected over the evaluation window. |
| Blocklist detection | `deliverability.blocklist_detection` | An IP address or domain associated with your sending has been found on a blocklist. |
| Suppression list growth spike | `deliverability.suppression_list_growth_spike` | Your SES suppression list is growing faster than normal, indicating a potential list quality issue. |
Default thresholds are based on AWS SES best-practice guidelines (bounce rate under 5%, complaint rate under 0.1%). You can adjust these per-domain or per-channel in notification preferences.
## SES Account Health
These notifications relate to the health and limits of your AWS SES account. They often require immediate action to avoid sending interruptions.
| Type | Key | Description |
|------|-----|-------------|
| Sending quota approaching | `ses_account_health.sending_quota_approaching` | Your account is using 85% or more of its 24-hour SES sending quota. |
| Sending quota reached | `ses_account_health.sending_quota_reached` | Your account has hit the SES sending quota. No further emails can be sent until the quota resets. |
| Account review / probation | `ses_account_health.account_review_probation` | AWS has placed your SES account under review or probation. Sending may still work, but you should address the underlying issue. |
| Sandbox status change | `ses_account_health.sandbox_status_change` | Your SES account's sandbox status has changed (e.g., moved to production access or back to sandbox). |
| SES config sync failure | `ses_account_health.ses_config_sync_failure` | SendOps was unable to sync your SES configuration. This may indicate a permissions issue or API error. |
| Production access granted | `ses_account_health.production_access_granted` | AWS granted SES production access for your account — you can now send to unverified recipients. |
| Production access failed | `ses_account_health.production_access_failed` | An SES production access request timed out or was denied by AWS. |
| First send | `ses_account_health.first_send` | Your organization's first-ever outbound email has been sent. |
When SES places your account under review or probation, address the root cause immediately. Review bounce and complaint details in your [Deliverability Reports](/reports/deliverability-reports).
## Template Lifecycle
Template notifications track changes to your email templates, including syncs from your connected GitHub repository.
| Type | Key | Description |
|------|-----|-------------|
| Sync completed | `template_lifecycle.sync_completed` | A GitHub sync completed successfully and templates have been updated. |
| Sync failed | `template_lifecycle.sync_failed` | A sync from your connected [GitHub repository](/templates/github-integration) failed. Error details are included. |
| PR merged | `template_lifecycle.pr_merged` | A template pull request has been merged into your connected branch and the templates have been deployed. |
| Validation error | `template_lifecycle.validation_error` | A template on your connected branch failed validation after sync. Check the template for syntax or variable errors. |
| Rebuild completed | `template_lifecycle.rebuild_completed` | A configuration change has rebuilt and redeployed your templates. |
| SES export completed | `template_lifecycle.ses_export_completed` | An SES template export zip is ready for download. |
| SES export failed | `template_lifecycle.ses_export_failed` | An SES template export failed. |
| GitHub disconnected | `template_lifecycle.github_disconnected` | The connection to your GitHub repository has been lost. Re-connect from **Templates** to resume syncing. |
## Channel Events
Channel notifications inform you when the structure or health of your [Channels](/channels/understanding-channels) changes.
| Type | Key | Description |
|------|-----|-------------|
| Channel created | `channel_events.channel_created` | A new channel has been created. |
| Channel adopted | `channel_events.channel_adopted` | A channel discovered during sync has been adopted into SendOps. |
| Channel settings changed | `channel_events.channel_settings_changed` | A channel's configuration (engagement tracking, event destinations, etc.) has been updated. |
| Channel health degraded | `channel_events.channel_health_degraded` | A channel's health metrics have degraded below acceptable thresholds. |
| Channel paused | `channel_events.channel_paused` | A channel has been automatically paused due to health issues. |
| Domain removed | `channel_events.domain_removed` | A domain has been removed from a channel. |
| Domain verified | `channel_events.domain_verified` | A sender domain's identity has been verified in SES. |
| Tracking domain verified | `channel_events.tracking_domain_verified` | A custom tracking domain has been successfully verified. |
| Tracking domain failed | `channel_events.tracking_domain_failed` | A tracking domain verification attempt has failed. Check your DNS records. |
| Tracking domain drift | `channel_events.tracking_domain_drift` | A previously verified tracking domain's DNS records are no longer valid. |
## Reporting Digests
Digest notifications let you know when scheduled reports are ready.
| Type | Key | Description |
|------|-----|-------------|
| Daily digest | `reporting_digests.daily_digest` | Your daily sending summary is available. |
| Weekly digest | `reporting_digests.weekly_digest` | Your weekly deliverability and engagement report is available. |
| Monthly executive summary | `reporting_digests.monthly_executive_summary` | Your monthly executive report is available with full metrics for the period. |
## System & Infrastructure
System notifications indicate infrastructure-level problems that may affect data collection or integrations.
| Type | Key | Description |
|------|-----|-------------|
| EventBridge connection issue | `system_infrastructure.eventbridge_connection_issue` | The EventBridge connection to your AWS account has been lost. Events may not be reaching SendOps. |
| AWS connected | `system_infrastructure.aws_connected` | Your organization has completed its AWS/SES connection. |
| API error rate spike | `system_infrastructure.api_error_rate_spike` | The SendOps API is experiencing an elevated error rate. |
| Scheduled maintenance | `system_infrastructure.scheduled_maintenance` | A maintenance window has been scheduled. |
| CloudFormation drift | `system_infrastructure.aws_template_drift` | The CloudFormation stack has drifted from its expected configuration. See [AWS Integration](/aws-setup/connecting-aws). |
| Edge stack drift | `system_infrastructure.edge_stack_drift` | The deployed Edge stack (image CDN) is running an outdated template version. |
| High duplicate event rate | `system_infrastructure.duplicate_event_rate` | A higher than normal rate of duplicate events is being detected. |
| Claim conflict | `system_infrastructure.claim_conflict` | Two or more workspaces on the same AWS account each claim the same sending identity, so its analytics events are paused until one workspace adopts it. Sent to admins of every workspace involved. See [Shared AWS Accounts](/aws-setup/shared-accounts). |
## User Access
User access notifications are sent when team membership or permissions change. These are useful for security auditing alongside the [Audit Log](/team/audit-log).
| Type | Key | Description |
|------|-----|-------------|
| User invited | `user_access.user_invited` | A new team member has been invited to the workspace. |
| Role changed | `user_access.role_changed` | A team member's role has been changed (e.g., from Marketer to Admin). |
## Billing
Billing notifications relate to payment events and plan limits.
| Type | Key | Description |
|------|-----|-------------|
| Payment failed | `billing.payment_failed` | A payment attempt was unsuccessful. Update your payment method to avoid service interruption. |
| Payment action required | `billing.payment_action_required` | A payment requires additional authentication (e.g., 3D Secure). Complete the action to process the payment. |
| Invoice overdue | `billing.invoice_overdue` | An invoice is overdue. Update your payment method or contact support. |
| Payment method expiring | `billing.payment_method_expiring` | Your payment method is expiring soon. Update it to avoid failed payments. |
| Plan limit approaching | `billing.plan_limit_approaching` | You are approaching the limits of your current plan (seats, templates, or event retention). |
## Account Sync
Account sync notifications fire when SendOps's periodic sync of your AWS account detects that the set of domains, email identities, or channels it manages has changed.
| Type | Key | Description |
|------|-----|-------------|
| Entity discovered | `account_sync.entity_discovered` | New domains, email identities, or channels were found in your AWS account. |
| Entity detached | `account_sync.entity_detached` | A domain, email identity, or channel that SendOps was managing is no longer present in AWS. |
| Entity reattached | `account_sync.entity_reattached` | A previously detached entity has reappeared in your AWS account. |
## What's next?
- Learn how to control which of these notifications you receive in [Configuring Notifications](/notifications/configuring-notifications).
- Set up outbound webhooks under [Webhooks](/notifications/webhooks) for programmatic event consumption.
---
# Webhooks
Source: https://help.sendops.dev/notifications/webhooks
Markdown: https://help.sendops.dev/notifications/webhooks.md
Section: Notifications & Alerts
> Receive SendOps notifications programmatically via HTTP webhooks. Learn how to create endpoints, verify HMAC-SHA256 signatures, and handle retries.
Webhooks let you receive SendOps [notifications](/notifications/notification-types) as HTTP POST requests to an endpoint you control. This is useful for feeding alerts into incident management tools, triggering automation workflows, or syncing notification data with internal systems.
## How webhooks work
When a notification is triggered and you have a webhook configured for that [notification type](/notifications/notification-types), SendOps sends an HTTP POST request to your configured endpoint with a JSON payload describing the event. Each request includes an HMAC-SHA256 signature header so your server can verify the payload came from SendOps.
## Creating a webhook endpoint
In the SendOps dashboard, navigate to **Connections → Webhooks** in the sidebar and click **Add Endpoint**.
Provide the HTTPS URL where SendOps should deliver notifications. The endpoint must be publicly accessible and respond to POST requests.
Choose which notification types should be sent to this endpoint. You can select individual types or entire categories. See [Notification Types](/notifications/notification-types) for the full list.
You can create multiple webhook endpoints and route different notification types to each one.
After creating the endpoint, SendOps generates a unique **signing secret** for it. Copy this secret and store it securely — you will need it to verify incoming webhook payloads.
The secret is only shown once. If you lose it, you can rotate it from the endpoint settings. When you rotate, the previous secret keeps working for a 24-hour grace period so you have time to update your server without dropping deliveries (see [Managing endpoints](#managing-endpoints)).
Treat the webhook signing secret like a password. Store it in environment variables or a secrets manager — never hard-code it in your application source or commit it to version control. Anyone with the secret can forge valid-looking webhook payloads.
## Payload structure
Every webhook delivery sends a JSON payload with the following structure:
```json
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"notification_type": "deliverability.bounce_rate_threshold",
"severity": "warning",
"account_id": "x7y8z9a0-b1c2-3456-defg-hi7890123456",
"timestamp": "2026-03-14T10:30:00Z",
"title": "Bounce rate exceeded threshold",
"body": "Bounce rate is 5.2%, exceeding your configured threshold of 5.0%",
"payload": {
"rate_pct": 5.2,
"threshold_pct": 5.0,
"window_minutes": 60,
"count": 520,
"total_sent": 10000
}
}
```
| Field | Description |
|-------|-------------|
| `id` | A unique identifier (UUID) for this individual delivery. See the note below — it is **not** a stable event ID and is not suitable for deduplication. |
| `notification_type` | The notification type in `category.event_name` format. |
| `severity` | One of `info`, `warning`, or `critical`. |
| `account_id` | Your SendOps organization identifier (UUID). |
| `timestamp` | ISO 8601 / RFC 3339 timestamp of when the event occurred. |
| `title` | A human-readable title for the notification. |
| `body` | A human-readable description of the event. |
| `payload` | An optional object containing event-specific metadata. The shape varies by notification type — the example above shows the fields for `deliverability.bounce_rate_threshold`. |
A fresh `id` is generated for every webhook request — including each retry of the same underlying event. Two deliveries of the same event (for example, an initial attempt and its retry) will carry **different** `id` values, so you cannot use `id` to recognise a retry or to deduplicate. If you need idempotency, deduplicate on a combination of fields that are stable across retries, such as `notification_type`, `account_id`, and `timestamp`.
## Verifying webhook signatures
Every webhook request includes two headers for signature verification:
| Header | Description |
|--------|-------------|
| `X-SendOps-Signature` | The HMAC-SHA256 signature, prefixed with `v1=`. |
| `X-SendOps-Timestamp` | The Unix timestamp (seconds since epoch) when the request was sent. |
The signature is computed over the timestamp and the raw request body joined by a `.` separator. This binds the timestamp to the payload, preventing replay attacks.
Always verify this signature before processing a webhook payload to ensure the request originated from SendOps and was not tampered with in transit.
### Verification steps
1. Extract the `X-SendOps-Timestamp` header value.
2. Extract the raw request body as a byte string. Do not parse or transform it before verification.
3. Concatenate the timestamp, a literal `.` character, and the raw body: `timestamp + "." + body`.
4. Compute an HMAC-SHA256 digest of this string using your signing secret as the key.
5. Prefix the hex-encoded digest with `v1=` and compare it to the `X-SendOps-Signature` header using a constant-time comparison function.
6. If the signatures match, the payload is authentic. If they do not match, reject the request with a `401` status code.
### Pseudocode example
```
secret = get_env("SENDOPS_WEBHOOK_SECRET")
raw_body = request.get_raw_body()
timestamp = request.headers["X-SendOps-Timestamp"]
signed_content = timestamp + "." + raw_body
expected_signature = "v1=" + hmac_sha256(key: secret, message: signed_content).to_hex()
received_signature = request.headers["X-SendOps-Signature"]
if not constant_time_equal(expected_signature, received_signature):
respond(status: 401, body: "Invalid signature")
return
event = parse_json(raw_body)
# Process the event...
respond(status: 200)
```
Always use a constant-time string comparison function when checking signatures. A standard string equality check can leak information through timing differences, potentially allowing an attacker to forge valid signatures.
## Responding to webhooks
Your endpoint must return an HTTP `2xx` status code within **10 seconds** to acknowledge receipt. SendOps considers any non-`2xx` response or timeout as a delivery failure and will retry.
You do not need to process the event before responding. A recommended pattern is to store the raw payload in a queue and return `200` immediately, then process the event asynchronously.
## Retry policy
When a webhook delivery fails, SendOps retries up to 2 additional times:
| Attempt | Delay |
|---------|-------|
| Initial delivery | Immediate |
| 1st retry | 5 minutes |
| 2nd retry | 15 minutes |
After 3 total attempts, the delivery is marked as failed. You can manually resend failed deliveries from the delivery log, up to **30 days** after the original delivery — older deliveries can no longer be resent.
### Auto-disable on consecutive failures
If a webhook endpoint fails **10 consecutive deliveries** (across any notification types), SendOps automatically disables the endpoint to prevent further failed requests. The endpoint can be re-enabled from **Connections > Webhooks** after resolving the issue.
You can view delivery history and failure details for each endpoint in **Connections > Webhooks** by clicking on an endpoint.
SendOps guarantees **at-least-once delivery** for webhooks. In rare cases (e.g., your server responds with a `200` but the response is lost in transit), the same event may be delivered more than once. Because every delivery carries a fresh `id` (see [Payload structure](#payload-structure)), don't rely on `id` to deduplicate — instead make your handler idempotent using stable fields like `notification_type`, `account_id`, and `timestamp`.
## Managing endpoints
From **Connections → Webhooks**, you can:
- **Enable / disable** — temporarily stop deliveries to an endpoint without deleting it.
- **Edit subscriptions** — change which notification types are routed to the endpoint.
- **Rotate secret** — generate a new signing secret. The previous secret remains valid for 24 hours to give you time to update your server.
- **View delivery log** — inspect recent deliveries, including response status codes, latency, and payload contents.
- **Delete** — permanently remove the endpoint and all its delivery history.
## What's next?
- Review the full list of events you can subscribe to in [Notification Types](/notifications/notification-types).
- Configure which notifications reach you in [Configuring Notifications](/notifications/configuring-notifications).
---
# Lists
Source: https://help.sendops.dev/audience/lists
Markdown: https://help.sendops.dev/audience/lists.md
Section: Audience
> Static audiences with explicit membership. Create Lists and manage their members in the dashboard, via the API, or as code in a connected repository.
A **List** is a static audience: a set of contacts whose membership is **explicit**. A contact is in a List because it was added, and it stays there until it's removed. Lists don't change on their own — contrast that with a [Segment](/audience/segments), whose membership is recomputed automatically from a rule.
Lists live in the **Audience** area of the SendOps dashboard, and — along with Segments — they're what you actually send to.
Your [**contacts**](/audience/contacts) are every person SendOps knows about, and they belong to your AWS account: they're synced from the one contact list AWS SES gives that account. A **List** is a group *you* build on top of them, and it exists only in SendOps — AWS never sees it. You browse contacts; you send to Lists. See [Contacts are not lists](/audience/contacts#contacts-are-not-lists).
Anyone on your team can view Lists and their members. **Creating, editing, and deleting Lists — and adding or removing members — requires the "manage lists" permission** (`lists.manage`). Without it, the controls described below are hidden and the view is read-only. See [Members & roles](/team/members-and-roles).
## The Lists page
The **Lists** page shows every List in your organization. Each row shows:
- **Name** — the List's display name. Click a row to open its detail page.
- **Key** — a short, org-unique slug that stays stable even if you rename the List. Use it when you refer to a List from your own systems.
- **Source** — how the List was created: `ui` (made in the dashboard), `api`, `import`, or `topic_export` (a point-in-time snapshot of a [topic](/audience/topics)'s subscribers, captured into this List).
- **Members** — how many contacts are currently on the List.
- **Created** — when the List was created.
If you have the manage permission, a **New list** button sits at the top of the page, and each row has a menu to **Edit** or **Delete** the List.
If you don't have any Lists yet, the page explains where they come from and offers shortcuts to create one, read the **API docs**, or — if you haven't connected one — a **Connect a repo** button.
## Creating and editing a List
Click **New list** to create one. A List has just three fields:
- **Name** — the display name.
- **Key** — the stable slug. SendOps suggests one from the name; it must be unique in your org and **can't be changed after creation** (it's what your own systems and the API refer to).
- **Description** — optional notes about who the List is for.
Use a row's **Edit** action to change a List's name or description later. **Delete** removes the List and its membership (the contacts themselves are untouched); you'll be asked to confirm.
Deleting a List drops its membership and can't be undone. The contacts on it aren't deleted — only the List and the record of who was on it.
## List detail
Open a List to see its full metadata — key, member count, description, and when it was created and last updated — followed by its **Members** table:
- **Email** — the member's email address.
- **Contact ID** — the contact's unique identifier.
- **Added** — when the contact was added to the List.
Members are paginated; use **Previous** and **Next** to move through large Lists.
### Adding members
With the manage permission, the detail page has an **Add contacts** button that opens a dialog with three ways to add people:
- **Paste emails** — type or paste one or more email addresses. Addresses that don't match an existing contact are **created as new contacts** automatically.
- **Upload a CSV** — import a file of email addresses the same way. This path reads addresses only; to import attributes or topic preferences, use [Importing contacts](/audience/importing-contacts).
- **Pick existing contacts** — search your contacts and select them.
After it runs, the dialog reports a **per-row result** so you can see which addresses were added, which were already members, and which failed.
Adding an address here **never changes consent**. If the contact has globally unsubscribed, they stay unsubscribed — they're simply a member of this List who won't receive topic email. Membership and mailability are separate things; see [Consent & lifecycle mail](/sending-email/consent-and-lifecycle).
### Removing members
Each member row has a **Remove** action (manage permission required). Removing a contact takes it off this List only — it doesn't delete the contact or affect any other List or Segment.
## Other ways to manage Lists
Beyond the dashboard, you can manage Lists two other ways:
- **As code, in a connected repository.** Connect a GitHub repository and define your audience alongside your templates. SendOps validates and syncs it on every push. See [GitHub integration](/templates/github-integration).
- **Through the SendOps API.** Read List metadata and members with the [Lists & Segments API](https://developers.sendops.dev/api-reference/lists-segments). List *definitions* are read-only over the API, but you can add or remove a contact's **membership** inline when you create or update that contact — see the [Contacts API](https://developers.sendops.dev/api-reference/contacts) (scope `api.contacts.manage`).
Like Segments, a List has an **Active** or **Archived** status and can move between the two authoring paths: **Promote** a dashboard-managed List into your repository, or **Adopt** a git-backed one back into the app. If a git-backed List's source file is removed from the repo, the List becomes **Archived** — its members are kept, and it returns to **Active** if the file comes back. See [Managed vs git-backed authoring](/audience/managed-vs-git-authoring).
A **List** here is an audience you target. It is unrelated to the [Undeliverable List](/reports/undeliverable-list) and the AWS SES suppression list, which are deliverability safeguards that hold addresses you should *not* send to.
## What's next?
- [Segments](/audience/segments) — dynamic audiences defined by a rule instead of explicit membership.
- [Contacts](/audience/contacts) — the people a List is built from, and where they actually live.
- [Topics](/audience/topics) — opt-out consent streams, not to be confused with a List.
- [Attributes](/audience/attributes) — the custom contact fields that power Segment rules.
- [Lists & Segments API](https://developers.sendops.dev/api-reference/lists-segments) — read Lists and their members programmatically.
---
# Contacts
Source: https://help.sendops.dev/audience/contacts
Markdown: https://help.sendops.dev/audience/contacts.md
Section: Audience
> Who a contact is, where contacts actually live (the one contact list your AWS account owns), and why a contact is not a list.
A **contact** is a person: one email address, plus whatever [attributes](/audience/attributes) you know about them and whatever they've consented to receive. Contacts are the raw population SendOps works from — [Lists](/audience/lists) and [Segments](/audience/segments) are ways of *grouping* them.
Contacts live under **Infrastructure › Contacts** in the dashboard, next to [Topics](/audience/topics). That placement is deliberate, and it's the thing most worth understanding on this page: **contacts are not really yours — they're your AWS account's.**
## Contacts are not lists
This is the distinction that trips people up, so here it is plainly:
| | **Contacts** | **[Lists](/audience/lists)** |
|---|---|---|
| What it is | Every person SendOps knows about | A group *you* build to send to |
| Where it lives | Your AWS account's SES contact list | SendOps only — AWS never sees it |
| How many | Exactly one set, per AWS account | As many as you like |
| Who puts people in it | SES and SendOps, by syncing | You, the API, or a connected repo |
| Do you send to it? | **No** | **Yes** |
The short version: **you browse contacts; you send to lists and [segments](/audience/segments).** If you're reaching for the contact browser to pick who gets an email, reach for a list instead.
Adding someone to a List doesn't create a second copy of them, and removing them from a List doesn't delete them. There is one contact per email address, and Lists are just labels on top of it. Removing a contact from a List also does **not** unsubscribe them — see [Consent & lifecycle mail](/sending-email/consent-and-lifecycle).
## Where contacts come from
AWS SES gives an account exactly **one** contact list — not one per SendOps workspace, one per *AWS account*. SendOps uses whatever list your account already has, or creates one named `sendops-contacts` the first time it needs to. Your contacts and your [topics](/audience/topics) both live on it.
That has three consequences worth knowing:
- **Contacts arrive on their own.** SendOps syncs the SES contact list roughly hourly. Anyone already in it — including contacts added to SES outside SendOps entirely — shows up in your Contacts page without you importing anything.
- **Adding a contact in SendOps adds it to SES — but not instantly.** Create one in the dashboard, over the API, or by uploading a CSV, and SendOps stores it straight away, then mirrors it to your SES contact list in the background. AWS caps contact writes at roughly **one per second per account**, so a large import can be finished in SendOps while the AWS console is still catching up. Nothing here waits on that: segments, sends, exports and the Contacts page all read SendOps' own copy. The delay is only visible if you're watching the contact list in the AWS console, or another system reads SES directly.
- **Removing a contact from SES removes it here.** If a contact disappears from the SES contact list, the next sync detaches it from your workspace.
Because the contact list belongs to the AWS *account*, two SendOps workspaces connected to the **same** AWS account converge on the **same contacts and the same topics**. Your [Lists](/audience/lists) are still your own, so you can share an account and keep your audiences apart — but everyone on the account can *see* the whole contact pool. See [Shared AWS accounts](/aws-setup/shared-accounts).
## What SendOps keeps, and what SES keeps
Not everything about a contact goes to AWS. The split matters when you're reasoning about what a sync can and can't overwrite:
- **Email address and subscription state** — mirrored to the SES contact list.
- **[Attributes](/audience/attributes)** — SendOps only. SES never stores them, and a sync can never clobber them.
- **[Activities](/audience/activities)** — SendOps only.
- **[List](/audience/lists) and [Segment](/audience/segments) membership** — SendOps only. These have no AWS representation at all.
## Browsing contacts
The **Contacts** page is a browser, not a targeting tool. Search by address, and filter by topic, subscription status, global unsubscribe, or whether the address is currently [undeliverable](/reports/undeliverable-list). You can export the filtered set, and — with the manage permission — add a contact, import a CSV, or act on a selection in bulk.
Open a contact to see everything SendOps knows about that one person: their attributes, their topic subscriptions and global unsubscribe state, the Lists they're on, and their recent activity.
Streaming an [activity](/audience/activities) for an unknown address creates a *stub* contact so the event has somewhere to land. That contact is real and browsable, but it hasn't consented to anything — being in your contacts has never meant being mailable. See [Consent & lifecycle mail](/sending-email/consent-and-lifecycle#membership-is-not-mailability).
## What's next?
- [Lists](/audience/lists) — the groups you actually send to.
- [Segments](/audience/segments) — groups defined by a rule instead of by hand.
- [Topics](/audience/topics) — the consent streams that live on the same SES contact list.
- [Consent & lifecycle mail](/sending-email/consent-and-lifecycle) — what decides whether a contact actually receives a message.
- [Contacts API](https://developers.sendops.dev/api-reference/contacts) — read and write contacts programmatically.
---
# Importing contacts
Source: https://help.sendops.dev/audience/importing-contacts
Markdown: https://help.sendops.dev/audience/importing-contacts.md
Section: Audience
> Upload a CSV to create or update contacts in bulk — the column format, what a blank cell means, and why an import never changes consent you didn't ask it to change.
Importing a CSV is how you get a lot of [contacts](/audience/contacts) into SendOps at once — a first load from another tool, or a periodic refresh of [attributes](/audience/attributes) you keep in your own system.
An import is an **upsert**, not an insert. A row whose email already exists **updates** that contact rather than creating a duplicate, so re-uploading a corrected file is safe and running the same import twice does nothing the second time.
## Running an import
From **Infrastructure › Contacts**, choose **Import CSV**, pick your file, and confirm. SendOps shows a preview of the parsed columns before anything is written.
A single import handles up to **100,000 rows**.
You can also upload a CSV of addresses from a [List's](/audience/lists) **Add contacts** dialog. That path only reads email addresses — use the Contacts page for a file that carries attributes or consent.
## The column format
One column is required. Everything else is optional, and any column SendOps doesn't recognise becomes a custom attribute.
| Column | Meaning |
|---|---|
| `email` | **Required.** The contact's address. A row with a blank or malformed address is rejected. |
| `unsubscribe_all` | Optional. The global unsubscribe flag — see [below](#a-blank-cell-changes-nothing), because a blank cell here is meaningful. Accepts `true`/`false`, `yes`/`no`, `y`/`n`, or `1`/`0`. |
| `topic:{name}` | Optional, one column per [topic](/audience/topics). The cell must read `OPT_IN` or `OPT_OUT`. For a topic called *Product updates*, the header is `topic:Product updates`. |
| *anything else* | A custom [attribute](/audience/attributes), with the column header as the attribute name. |
Recognised headers (`email`, `unsubscribe_all`, and the `topic:` prefix) are matched **case-insensitively**. Topic names and attribute names keep the case you write them in, so `topic:Product updates` and an attribute column `plan_tier` arrive exactly as spelled.
A small example — two attributes, one topic, and no consent column at all:
```csv
email,plan,last_signin_at,topic:Product updates
jane@example.com,pro,2026-07-28,OPT_IN
sam@example.com,free,2026-06-02,OPT_OUT
```
The **Export** button on the Contacts page writes the same shape it reads — `email`, `unsubscribe_all`, `created_at`, `updated_at`, then a column per topic and per attribute. Exporting, editing, and re-importing is a supported round trip. Note that an export *does* carry explicit `unsubscribe_all` values, so re-importing one applies exactly the consent state it captured.
## A blank cell changes nothing
This is the part worth reading carefully, because it's about consent.
**Blank means "leave it alone."** For every optional column, an empty cell — or a column you simply didn't include — means SendOps makes **no change** to that field on an existing contact:
- No `unsubscribe_all` column, or an empty cell in one → the contact's global unsubscribe is **untouched**.
- An empty `topic:{name}` cell → that topic preference is untouched.
- An empty attribute cell → that attribute is untouched.
So a spreadsheet that only refreshes attributes updates only attributes. It cannot resurrect an unsubscribe, and it cannot quietly opt anyone back in.
Changing someone's global unsubscribe requires you to say so explicitly, by putting `true` or `false` in the cell. Re-subscribing a person who opted out is a deliberate act, so it takes a deliberate `false`.
Before that date, an import whose file had **no** `unsubscribe_all` column treated every row as `false` and **cleared the global unsubscribe** on every contact in the file — up to 100,000 people, reported as a clean success. An ordinary attribute refresh could re-subscribe your entire audience.
That's fixed, and no action is needed on your part. But if you ran an attribute-only import before 31 July 2026 and your unsubscribe counts look wrong, that's the likely cause — check the [Contacts](/audience/contacts) page filtered by global unsubscribe against your own records.
Adding someone to a [List](/audience/lists) has never been a consent change either, and isn't one now: dropping a globally-unsubscribed address into a List leaves them unsubscribed.
## What happens to bad rows
A problem in one row fails **that row only** — the rest of the file still imports. After the run you get a per-row report naming what was rejected and why. Rows are rejected for a missing or malformed `email`, an `unsubscribe_all` cell that isn't a recognised true/false value, or a `topic:` cell that isn't `OPT_IN` or `OPT_OUT`.
If the **same email appears more than once** in a file, the last occurrence wins and the import warns you. The whole file is refused only if it can't be read at all, has no `email` column, or exceeds the 100,000-row cap.
## After the import
Imported contacts are usable in SendOps **immediately** — they show up in the Contacts browser, count towards [Segments](/audience/segments), and can be sent to.
Mirroring them into your AWS account's SES contact list happens in the background, because AWS caps contact writes at roughly **one per second per account**. A large first-time import can therefore take a while to appear fully in the AWS console even though it's complete in SendOps. Nothing waits on it — see [where contacts actually live](/audience/contacts#where-contacts-come-from).
## What's next?
- [Contacts](/audience/contacts) — what a contact is and where it lives.
- [Attributes](/audience/attributes) — registering the fields your columns write into.
- [Topics](/audience/topics) — the consent streams `topic:` columns set.
- [Syncing external state](/audience/syncing-external-state) — keeping contacts current from your own system, instead of by file.
- [Contacts API](https://developers.sendops.dev/api-reference/contacts) — the programmatic equivalent, including bulk import.
---
# Topics
Source: https://help.sendops.dev/audience/topics
Markdown: https://help.sendops.dev/audience/topics.md
Section: Audience
> Named, opt-out consent streams backed by your SES contact list. Create Topics in the dashboard or as code, and use them to give recipients granular control over what mail they receive.
A **Topic** is a named stream of mail a recipient can opt out of on its own — "Product updates," "Weekly digest," "Onboarding." Topics are what make an unsubscribe **granular**: a contact can leave one topic without going globally unsubscribed. Every Topic is backed by a topic on your connected AWS SES contact list, so subscription preferences are enforced by SES itself, not just tracked in SendOps.
Topics live under **Infrastructure › Topics** in the SendOps dashboard, alongside [Contacts](/audience/contacts) — because both are read off the single contact list AWS SES gives your *account*, rather than being definitions you author. See [Consent & lifecycle mail](/sending-email/consent-and-lifecycle#topics-are-opt-out-streams) for how a Topic fits into the wider consent model that decides whether a send actually goes out.
A recipient can leave a **Topic** — that's what the unsubscribe page offers them. They can't leave a [**List**](/audience/lists): a List is how *you* choose who a send goes to, and it never appears to recipients. Opting out of a Topic drops someone from a send even if they're on the List it targeted.
## The Topics page
The **Topics** page lists every Topic in your organization. Each row shows:
- **Display name** — the human-readable name shown to recipients on the unsubscribe page.
- **Name** — the stable, org-unique key your own systems and Segment rules refer to (`subscribed to ""`).
- **Default subscription** — whether a newly imported or created contact starts **opted in** or **opted out** of this Topic (see [Default subscription status](#default-subscription-status) below).
- **Subscribers** — how many contacts currently have an active subscription to the Topic.
- **Origin** — `Managed` (created in the dashboard or via the API) or `Git` (declared in your connected repository).
- **Status** — `Active` or `Archived`.
## Creating and editing a Topic
A **managed** Topic has four fields:
- **Name** — the stable key. Used in Segment rules and referenced by your own systems; choose it carefully, as it's what identifies the Topic everywhere else.
- **Display name** — what recipients see on the unsubscribe page. Defaults to the name if you don't set one.
- **Description** — optional notes on what mail this Topic covers.
- **Default subscription status** — `Opted in` or `Opted out` (see below).
Archiving a Topic hides it from the picker used when composing a send, without deleting existing subscriber preferences or history.
### Default subscription status
**Default subscription status** governs whether a contact counts as **subscribed** or **not subscribed** to the Topic when they have never made an explicit choice about it themselves:
- **Opted in** — contacts are subscribed unless they opt out. Use this for topics recipients would reasonably expect by default (e.g., account notices grouped under a topic).
- **Opted out** — contacts are **not** subscribed; they have to actively opt in before you can send them that topic's mail. Use this for anything closer to pure marketing.
This is what SendOps enforces when it sends: a contact with no explicit choice on an **Opted out** Topic will not be mailed on it, and is not counted among its subscribers.
#### Changing the default moves contacts
The default is not stamped onto contacts when they're added — it's applied whenever SendOps asks "is this contact subscribed?". Most contacts never set a preference explicitly (a preference is only recorded when someone acts: a click on the unsubscribe page, an API call, a `topic:` column in a CSV import, or a preference synced from SES).
So **changing the default immediately moves every contact who has never chosen for themselves**, in both directions:
- **Opted in → Opted out** shrinks the Topic's audience, often sharply. Contacts who were being mailed will stop being mailed.
- **Opted out → Opted in** grows it. Contacts who never asked for this Topic's mail will start receiving it — make sure that's a defensible thing to do before you flip it.
Contacts who *have* made an explicit choice keep it and are never affected by the default.
## Using Topics in sends and rules
- **Broadcasts** send under a Topic so the topic tier of consent is enforced — see [Broadcasts](/sending-email/broadcasts).
- **Drip Workflow** `send` steps choose a Topic with `via topic ""`, or inherit a template's default — see [Sends, consent & approval](/workflows/sends-and-approval).
- **Segment rules** select on subscription state with `subscribed to ""` and `opted out of ""` — see [Segment syntax](/audience/segment-syntax#consent-and-membership).
## Managing Topics as code
Like [Lists](/audience/lists), [Segments](/audience/segments), and [Attributes](/audience/attributes), Topics can be authored **as code** in your connected repository instead of the dashboard. The `topics` key of your `sendops.json` manifest is a **path to a JSON file** — relative to the manifest, no `..` segments, ending in `.json` — and that file holds a map of topic name to definition:
```json
// sendops.json
{
"topics": "audience/topics.json"
}
```
```json
// audience/topics.json
{
"product-updates": {
"display_name": "Product Updates",
"description": "New features and improvements.",
"default_subscription_status": "OPT_OUT"
},
"weekly-digest": {
"display_name": "Weekly Digest",
"default_subscription_status": "OPT_IN"
}
}
```
Unlike Segments (one file per segment, referenced individually in the manifest), all of an org's git-backed Topics live together in the single file `topics` points at. Each key is the Topic's `name`; `display_name` and `description` are optional, but **`default_subscription_status` is required** — it decides who may be mailed, so it has to be stated rather than inherited. A Topic that omits it fails the sync with an error naming the Topic.
A git-backed Topic is **read-only in the app**, following the same [managed vs git-backed authoring](/audience/managed-vs-git-authoring) model as the rest of your audience definitions — edit it in the repository and open a pull request; the next sync brings the change in. You can **Promote** a managed Topic into git, or **Adopt** a git-backed one back into the dashboard.
Promoting or adopting a Topic only changes where its *definition* is authored (display name, description, default). Subscriber preferences always live on the underlying SES contact list and are unaffected by which side owns the definition.
## What's next?
- [Consent & lifecycle mail](/sending-email/consent-and-lifecycle) — the full consent model a Topic plugs into.
- [Managed vs git-backed authoring](/audience/managed-vs-git-authoring) — the shared model behind Promote and Adopt.
- [Segment syntax](/audience/segment-syntax#consent-and-membership) — selecting contacts by Topic subscription.
- [Lists](/audience/lists) — static audiences with explicit membership.
---
# Segments
Source: https://help.sendops.dev/audience/segments
Markdown: https://help.sendops.dev/audience/segments.md
Section: Audience
> Dynamic audiences defined by a rule. SendOps evaluates membership automatically from each contact's attributes and engagement; the dashboard shows the rule, status, and current members.
A **Segment** is a dynamic audience: instead of listing members one by one, you define a **rule** (a *predicate*), and SendOps works out who matches. As contacts' attributes and engagement change, SendOps re-evaluates the Segment and updates its membership — you never add or remove members by hand. Compare this with a static [List](/audience/lists), whose membership is fixed until you change it.
Segments live in the **Audience** area of the SendOps dashboard.
A Segment is authored one of two ways: **managed** — created and edited in the SendOps dashboard (or through the API) — or **git-backed** — defined as code in a connected repository and read-only in the app. You can move a Segment between the two at any time. See [Managed vs git-backed authoring](/audience/managed-vs-git-authoring) for the full picture; either way, membership is always evaluated automatically from the rule.
## The Segments page
The **Segments** page lists every Segment in your organization. Each row shows:
- **Name** and **Key** — the display name and a stable, org-unique slug.
- **Status** — see [Segment status](#segment-status) below.
- **Evaluation** — how membership is kept current: **Incremental** (updated as events arrive), **Sweep** (recomputed on a schedule), or both.
- **Members** — the size of the Segment at its last evaluation.
- **Created** — when the Segment was created.
### Segment status
| Status | Meaning |
|---|---|
| **Active** | The Segment is valid and being evaluated. |
| **Paused** | Evaluation is stopped; membership is frozen at the last run. |
| **Invalid** | The rule no longer compiles, so it can't be evaluated. The detail page explains why. |
| **Archived** | A repository-managed Segment whose source file was removed. |
The member count reflects the **last evaluation**, not this instant. A **Paused** or **Invalid** Segment isn't being re-evaluated, so its membership stays frozen at the last good run. Always check the status before treating a count as current.
## Segment detail
Open a Segment to see its metadata (key, member count, description, when it was created, and when it was **last evaluated**), plus:
- **The predicate** — the rule that defines membership. For a **git-backed** Segment it's shown in a read-only, copyable code block; for a **managed** Segment, use **Edit** to change the rule right there in the built-in editor. If the Segment is **Invalid**, an alert above it explains what's wrong.
- **Source** — if the Segment is managed in a connected repository, a **Managed in git** badge appears with the source file path, a link to **edit it in GitHub**, and when it last synced. Otherwise the Segment is **managed** — created and edited here in the app.
- **Members** — a paginated table of the contacts currently in the Segment, with each member's **Email**, **Contact ID**, and **Entered** time (when they most recently entered the Segment).
### Pausing and resuming
If you have the **"manage segments" permission** (`segments.manage`), the detail page of a **managed** Segment shows a **Pause** (or **Resume**) button:
- **Pause** stops SendOps from re-evaluating the Segment. Its membership freezes at the last run and its status becomes **Paused** — useful when you want to hold a Segment steady or stop churn while you investigate something.
- **Resume** puts an active evaluation back in place; the Segment returns to **Active** and starts updating again.
You'll be asked to confirm before the status flips. Pause/resume isn't available for **git-managed** Segments — control those from the connected repository instead.
## How the rule works
A Segment's predicate is an expression that's either true or false for each contact. It can reference the contact's custom [attributes](/audience/attributes), their engagement events (opens, clicks, and so on), and their subscription state. For example, a rule like `attr.plan = "pro" and attr.region = "eu"` selects every contact whose `plan` attribute is `pro` and whose `region` is `eu`.
You write rules in **SendQL** — see the full [Segment syntax reference](/audience/segment-syntax) for every term, operator, and worked examples. Because the rule references attributes by name, the [attribute registry](/audience/attributes) is what defines the vocabulary available to your Segments.
## Where Segments come from
You author a Segment one of three ways, and you can move it between them at any time:
- **In the dashboard.** Create a Segment from the Segments page — or the **Create segment** action in the ⌘K command bar — and write its rule in the built-in editor. The editor checks the rule as you type, previews how many contacts it matches, and keeps a version history you can restore from. Managed Segments stay fully editable here.
- **As code, in a connected repository.** Define Segments and attributes alongside your templates; SendOps validates the rule and syncs it on every push, and a removed source file moves the Segment to **Archived**. See [GitHub integration](/templates/github-integration).
- **Through the SendOps API.** Read Segments and their members, and **preview a rule** (see how many contacts it matches) before committing to it. See the [Lists & Segments API reference](https://developers.sendops.dev/api-reference/lists-segments).
## What's next?
- [Attributes](/audience/attributes) — the custom contact fields your rules reference.
- [Lists](/audience/lists) — static audiences with explicit membership.
- [Lists & Segments API](https://developers.sendops.dev/api-reference/lists-segments) — read Segments and preview predicates programmatically.
---
# Segment syntax
Source: https://help.sendops.dev/audience/segment-syntax
Markdown: https://help.sendops.dev/audience/segment-syntax.md
Section: Audience
> SendQL is the language you write Segment rules in. It selects contacts by their attributes, subscription state, and — uniquely — their raw engagement events. This reference covers the full syntax with worked examples.
**SendQL** is the language you write [Segment](/audience/segments) rules in. A rule is a single expression that is either true or false for each contact; the contacts it's true for are the Segment's members. SendOps re-evaluates the rule automatically as your contacts change.
SendQL's distinctive feature is that it treats **engagement events as a first-class source**: you can select on the raw stream of opens, clicks, sends, bounces, and so on — not just precomputed rollups — with time windows and property filters.
This page covers SendQL as you use it in SendOps. The language itself has its own
home — [sendlang.com/docs/sendql](https://www.sendlang.com/docs/sendql) —
where the complete grammar, every operator, and the parser are documented, alongside
its companion language [SendFlow](https://www.sendlang.com/docs/sendflow).
## Structure
A rule is built from **terms** (see the sections below) combined with boolean operators:
- `and`, `or`, `not`
- parentheses `( … )` for grouping
Precedence, loosest to tightest, is `or` → `and` → `not`. So `a or b and c` means `a or (b and c)`. Use parentheses when in doubt:
```sendql
(attr.plan = "pro" or attr.plan = "team") and not suppressed
```
## Attributes
Reference a contact's custom [attributes](/audience/attributes) as `attr.`.
| Form | Example |
|---|---|
| Comparison | `attr.score >= 10` |
| Equality / inequality | `attr.plan = "trial"`, `attr.plan != "trial"` |
| In a set | `attr.country in ["US", "CA", "MX"]` |
| String match | `attr.email ends with "@acme.io"`, `attr.name starts with "A"`, `attr.title contains "VP"` |
| Presence | `has attr.company` — the attribute is set |
| Presence (explicit) | `attr.tier is known`, `attr.tier is unknown` |
Comparison operators are `=`, `!=`, `<`, `<=`, `>`, `>=`. Ordering operators (`<`, `<=`, `>`, `>=`) apply to numbers, dates, and durations. Both sides of a comparison must be the same type — each attribute's type comes from [the attributes you've defined](/audience/attributes), so `attr.score > "ten"` is rejected when validating.
## Age
To select on how long ago a **date attribute** occurred, subtract it from `now`:
```sendql
now - attr.signup_date > 7d
now - attr.signup_date between 3d and 14d
```
The first matches contacts who signed up more than 7 days ago; the second, those 3–14 days in. The left side must be a date attribute; the right side is a [duration](#durations-and-dates).
## Events and activities
Behavioral terms select on things that *happened* to a contact. There are two kinds of source, and they're written differently:
- **Events** are the SES message signals SendOps tracks automatically. The vocabulary is **fixed** — exactly these eight bare names: `send`, `delivery`, `open`, `click`, `bounce`, `complaint`, `reject`, `delivery_delay`.
- **Activities** are custom signals your integration records through the [Activities API](/audience/activities) — `purchase`, `login`, whatever your product knows. An activity is **always written with the `activity.` prefix**: `activity.purchase`.
So `exists(order)` is a validation error — there is no built-in `order` event, and bare names don't reach into your custom signals. A custom order signal is `exists(activity.order)`.
Both sources use the same four shapes:
| Shape | Meaning | Example |
|---|---|---|
| `count() N` | how many times it happened | `count(open within 30d) >= 3` |
| `exists()` | it happened at least once | `exists(activity.purchase within 7d)` |
| `sum\|avg\|min\|max( of ) N` | aggregate a numeric field | `sum(amount of activity.order within 90d) > 500` |
| `last\|first()