Sending and delivery
Your sending domain
Section titled “Your sending domain”Nothing sends until you verify a domain you own. Add it, install the DNS records Cowliss gives you, and the status follows on its own:
cow domains create --domain mail.example.comcow domains list # find the dom_ id of the domain you just addedcow domains get dom_… # the records to install, each with its own statuscow domains verify dom_… # re-check now instead of waitingThe records cover signing, the bounce return path, a policy record, and one _cowliss-challenge TXT record that proves the domain is yours. Each name verifies on its own, and a domain that lapses stops sends rather than sending unsigned mail.
Send from a subdomain such as mail.example.com or notify.example.com, not from example.com itself. Your root domain carries the reputation of your team’s own mailboxes, and automation mail sent from it spends that reputation: a spam complaint about a campaign can push your colleagues’ everyday email into spam folders. A subdomain keeps the two apart. Cowliss still lets you add a root domain, but warns when you do and suggests the subdomain: the CLI prints the warning, and the API answers with a root_domain entry in meta.warnings.
How many sending domains an organization can hold depends on its plan. A domain holds its place from the moment you add it, verified or not, until you delete it.
Who your mail comes from
Section titled “Who your mail comes from”An automation names its own address in its definition’s from:
{ "trigger": { "event": "invoice_paid" }, "purpose": "transactional", "steps": []}from is required and sits on a verified domain: the schema refuses an automation without one, and the push refuses an unverified domain. A single sendEmail step can name its own. Replies go to the from address; a send from your own code can set a replyTo that is any address.
The gates every send passes
Section titled “The gates every send passes”A send is evaluated when it runs, not when the automation started, so someone who unsubscribes on day two of a five-day automation is respected on day two. The first gate that stops it decides the outcome, and a stopped send does not throw: it resolves with a skipped_* status carrying the reason.
The address is the recipient’s email trait as it stands when the send runs, else their newest email identifier, so an identify that changes traits.email moves every later send, including those of automations already running. A profile with neither is not mailed: if no gate stops the send first, it fails, and the delivery log says the profile has no email address.
| Gate | What it asks |
|---|---|
| Automation enabled | Is this automation turned on? |
| Sending paused | Is your organization’s sending paused? |
| Suppressed | Is this address on the suppression list? |
| Credit | Does the organization have credit left? |
| Frequency cap | Has this person already had 5 emails they could leave in the last rolling day? |
| Consent | Is the template’s purpose on for this person, and so is its root? |
| Sending domain | Is the from address on a domain you have verified? |
| Spam check | Does the rendered message read as spam? |
The spam check comes last because it reads the message exactly as it would leave, with every prop filled in and every link in place, so a link passed in through a prop is checked too. It applies to every email, receipts included. A message that scores as spam is skipped_spam, and the delivery log keeps its score and the names of the rules that matched, so you can see what to change. Broadcasts are checked per recipient, and an API send is refused with a 422.
Consent reads the purpose the template declares, never the automation’s, and it reads both halves of the path:
| Template purpose | Sent when | Unanswered means |
|---|---|---|
transactional |
Always: consent and the frequency cap are skipped | Nothing to answer |
transactional.<name> |
The name and transactional are both on |
On |
marketing.<name> |
The name and marketing are both on |
Off |
A run of skipped_consent on marketing mail usually means the grant never arrived: pass consent on the identify call your signup already makes, or set it from the dashboard. A version pushed with an older CLI fails its send with “Pushed with an older CLI. Push again to update.” rather than being sent under a guess.
The suppression list
Section titled “The suppression list”An address is suppressed when mail to it hard-bounces or the recipient marks a message as spam. Every later send to it is skipped before it costs anything. Fake and disposable domains, such as example.com or a throwaway mailbox service, are always treated as suppressed.
A complaint also revokes consent. On marketing mail it turns off marketing and every name your app declares under it. On a transactional.<name> email it turns off that name alone, and on bare transactional mail it changes nothing, since that mail cannot be left. A bounce does not: the address does not work, which says nothing about what the person wanted.
cow suppressions delete sup_… # admin onlyRemoving an entry is an admin action, for an address you know was fixed. It is not a way around a complaint.
Unsubscribing
Section titled “Unsubscribing”Every email whose purpose someone can leave, anything but bare transactional, carries the headers that put a one-click unsubscribe button in the recipient’s mail client, and the endpoint behind them revokes consent for the purpose the mail was sent under. That is the same flag the consent gate reads, and the same one you set from identify. Unless the template renders its own links, the body also carries a footer saying who it was sent to and why, with links to update email preferences or leave this purpose, and your app’s name, website and postal address. Leaving everything, from the preferences page, turns off both roots, and receipts keep arriving. The page after the click can undo it. See Unsubscribe links and Data protection.
The delivery log
Section titled “The delivery log”Every attempt writes one row, whether it went out or not:
| Status | Meaning |
|---|---|
sent |
Handed off to the mail provider. |
delivered |
Accepted by the recipient’s server. |
bounced |
Rejected. A hard bounce also suppresses the address. |
complained |
Marked as spam. Outranks a bounce, and revokes consent. |
failed |
Cowliss could not attempt it. |
skipped_* |
A gate stopped it: skipped_disabled, skipped_paused, skipped_suppressed, skipped_quota, skipped_frequency_cap, skipped_consent, skipped_domain, skipped_spam, skipped_ssrf. |
would_* |
A dry run. Nothing was sent. |
A late delivered never overwrites a bounced, so a row’s final status is the truthful one. Webhooks share the log, where a 2xx from your endpoint is terminal at delivered.
cow deliveries list --status bounced --since 2026-09-01T00:00:00Zcow deliveries list --automation abandoned-checkout --channel emailcow deliveries get dlv_…Reference
Section titled “Reference”- Sending domains: claim, DNS records, verification status.
- Deliveries: the log, its filters, and the public feedback and unsubscribe endpoints.
- Suppressions: listing and removal.