Skip to content

Sending and delivery

Authoring a template is one half of email. This is the other: what your organization has to set up once, what every individual send is checked against, and where you look when someone says they did not get it.

Until you verify a domain, sends go out from a shared fallback domain. That works for development and for a first look, and it is the wrong thing to grow on: mail from a domain you own is what reputation, alignment, and replies all depend on.

Claim the domain, install the DNS records Cowliss gives you, and the status follows on its own:

Terminal window
cow domains create --domain mail.example.com
cow domains list # find the dom_ id of the domain you just claimed
cow domains get dom_… # the records to install, each with its own status
cow domains verify dom_… # re-check now instead of waiting

The record table covers signing keys, the return path for bounces, and a recommended policy record. A claim is exact and global, so a domain a neighbour of yours already verified conflicts rather than silently sharing. Verification is checked when a send is prepared, so a domain that falls out of verification stops sends with a typed skip instead of sending unsigned mail.

A send call is an attempt. It is evaluated the moment it runs, in this order, and the first gate that stops it decides the outcome:

Gate What it asks
Journey enabled Is this journey turned on in this environment?
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 marketing emails in the last rolling day?
Consent Has this person consented to the journey’s purpose?
Sending domain Is there a usable verified domain, or the fallback?

A transactional template skips the frequency cap and the consent gate by design, because a receipt is not something anyone opted into. Every other gate still applies to it. See Email templates for how a template declares its class.

Evaluating at send time rather than at trigger time is deliberate: someone who unsubscribes on day two of a five-day journey is respected on day two, without anyone cancelling the execution.

A stopped send does not throw. The call resolves with a skipped_* status, so a journey keeps its shape, and the reason is on the delivery row.

An address lands on the suppression list when mail to it hard-bounces or when the recipient marks a message as spam. Every later send to that address is skipped before it costs anything, and the dashboard can say why.

Suppression and consent are separate facts. A complaint revokes consent (that person asked to stop hearing from you) and suppresses the address. A hard bounce only suppresses it: the address does not work, which says nothing about what the person wanted.

Terminal window
cow suppressions list --q [email protected]
cow suppressions delete sup_… # admin only

Removing an entry is a deliberate admin action, for the case where you know the address was fixed. It is not a way around a complaint.

Marketing mail carries the headers that put a one-click unsubscribe button in the recipient’s mail client, and the endpoint behind it revokes consent for the purpose the mail was sent under. That is the same consent flag the gate above reads, so the next journey to try is stopped for the right reason rather than by a filter you maintain.

Consent lives on the profile and you own it. You can set it from identify, and you should when your own signup form is where the person actually agreed. See Data protection.

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_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: a 2xx from your endpoint is its confirmation, and it is terminal at delivered.

Terminal window
cow deliveries list --status bounced --since 2026-09-01T00:00:00Z
cow deliveries list --journey abandoned-checkout --channel email
cow deliveries get dlv_…

The delivery log is the answer to “did they get it”, and the execution page is the answer to “why did the journey send it”. Both are per environment, so a development run is never mixed into your production numbers.

Sends from the development environment are captured rather than delivered, which is what makes it safe to run a whole journey against a real-looking profile. See Environments and Testing journeys.