Part 1 — Broadcasts
A broadcast is one email sent to many people, persisted end to end: you draft it, schedule it, send it, then read back who received it, who opened it, and which links they clicked.The lifecycle
draft → scheduled → sending → sent, with canceled and failed as exits.
Omit scheduled_at and you get a draft. Pass a future timestamp and it becomes scheduled — a cron picks it up when it comes due, so a deploy or restart can’t strand it. Pass send_now: true and it goes out immediately. A draft or scheduled broadcast stays editable; once it starts sending, the rendered body and recipient list are frozen so what you report can’t drift from what recipients actually received.
Choosing an audience
audience is how you say who receives a broadcast. Filters combine as a union and are deduplicated by address:
Addresses on your suppression list — hard bounces, spam complaints, and unsubscribes — are dropped before the send is counted, so
stats.recipients reflects what the provider will actually attempt. A broadcast with no reachable recipients is rejected rather than silently sending to nobody.
Content
Every broadcast carries a per-recipient unsubscribe link and the RFC 8058
List-Unsubscribe headers that let mailbox providers show a native “Unsubscribe” button. An opt-out adds that address to your suppression list, so it is honoured by every send path afterwards. This is not optional and cannot be turned off — bulk marketing mail requires it, and it is what protects your sending reputation.With styled: false the link is appended under your body. To place it yourself, put {{unsubscribe_url}} in an href and it is replaced with the recipient’s link instead.subject, preview_text and body take the live variables: customer.first_name, customer.last_name, customer.email, business.name and customer.fields.<key>, each with an optional | default: "…". They are filled per recipient from their customer record when each batch of 100 is handed to the provider; html_body keeps the rendered layout with the variables still in it. An address in to that belongs to one of your customers is linked to them; one that doesn’t renders every variable blank or as its default. Each recipient’s values are read once, before their batch is first handed over, so a provider retry sends the same email even if the customer changed in between. See Preview and test sends to check the result before sending. The snapshot variables (product.*, order.*, tag.name, form.name) and anything else in {{ }} fail validation with 422 validation_error when the broadcast is saved; {{unsubscribe_url}} is accepted in body only.
Worked example: broadcast to all paid customers
Reading the results
stats is embedded on every broadcast and also available on its own endpoint. Rates are fractions of sent, not of recipients — a recipient never handed to the provider can’t have opened anything.
opened and clicked count distinct recipients. For per-person detail use GET /broadcasts/{id}/recipients, and for per-link detail use GET /broadcasts/{id}/link-clicks, which reports both total clicks and unique_clicks.
Subscribe to broadcast.sent, broadcast.canceled, and broadcast.failed to react without polling. See Webhooks.
Open and click tracking
Tracking is off until the account has its own verified email domain, and turns itself on from there — no configuration. Click tracking works by rewriting every link in the body to redirect through a tracking host. Whose host that is decides whether it costs you deliverability:- URL blocklists score the hosts appearing in a message body, so a tracking host shared across many senders carries whatever reputation the worst of them earns.
- Links pointing somewhere other than the domain the mail is authenticated as is structurally what phishing looks like, and inbox providers weigh that alignment.
click.yourdomain.com and publishes the CNAME for it — written straight into your DNS when Crevio hosts the zone, or shown alongside the other records for you to add when it doesn’t. Once that record resolves, click and open tracking switch on, links stay on your domain, and the reputation is yours alone.
Until then stats.clicked and link-clicks stay empty. That’s deliberate: sending on the shared platform domain and tracking through a shared host is the configuration most likely to cost inbox placement, and click data isn’t worth that trade.
If something already lives at
click.yourdomain.com, Crevio leaves it alone rather than overwriting it — the record is surfaced in domain settings instead, and tracking stays off until it’s resolved.Sending limits
Broadcasts draw on a per-account daily send limit set by your plan. Exceeding it returns429 with quota_exceeded; a scheduled broadcast that hits the cap stays scheduled and is retried after the limit resets at UTC midnight, rather than failing outright.
Part 2 — Sequences
A sequence is a timed series of emails each person moves through on their own clock: a welcome series for new subscribers, a nurture series for leads, a follow-up after a purchase. Each step waits itsdelay after the previous send (after enrolling, for the first step). People join on the sequence’s trigger, or by hand, and leave when they finish, unsubscribe, bounce, or meet an exit rule.
Triggers
Nobody joins the same sequence twice. Existing customers don’t join when a sequence is created; add them through the enrollments endpoint. Test-mode orders and bulk-imported tags enroll no one.
Exit rules
People leave a sequence early when they unsubscribe or their address bounces or complains, which stops every sequence they’re in. On top of that,exit sets:
on_purchase—anypaid order, or only orders withproduct_ids(products). Only orders placed after enrolling count, so the order that enrolled someone never exits them. Subscription renewals count as purchases.tag_ids— getting any of these tags.on_reply— a reply from the person. Replies are only seen when they reach a Crevio inbox, so withon_replyon and noreply_to, replies go to the account’s Crevio inbox.
Send days and timing
send_days limits sending to chosen weekdays in the business’s time zone. A step that comes due on another day waits for the next allowed day at the same local time. Delays are exact durations, so a one-day delay can move an hour across a daylight-saving change. Steps are checked every minute.
Editing a live sequence
Edits never resend a step and never reach someone who already passed that point:- Add a step — people waiting for the step now at its place get the new step first. People past it skip it. People who finished never get a step appended later.
- Remove a step — people waiting for it move on to the next step, timed from their last send, or finish if it was the last. The only step of an active sequence can’t be removed (
409 sequence_needs_a_step). - Move a step — works like removing it and adding it at the new place. Someone who already received it doesn’t get it again.
- Change a delay — everyone waiting for that step is re-timed from their last send. If the new time has passed, the step goes out within a minute.
- Change content — applies to every later send of that step.
PATCH /sequences/{id} rejects steps with use_step_endpoints.
Variables
A step’ssubject, preview_text and body take variables, filled per recipient when the step is sent. The syntax is Liquid’s:
customer.* (fields included) and business.name are read when each email is sent. product.*, order.*, tag.* and form.* are a snapshot taken at enrollment, so a later rename or deletion doesn’t change what queued emails say. Form answers are not variables: they are keyed by form field id and labelled with free text a merchant can edit, so they have no stable name. Copy an answer onto the customer with PATCH /customers/{id} (metadata) to use it as a field. A blank value renders as nothing, or as its default: {{ customer.first_name | default: "there" }} (single or double quotes). Spaces inside the braces are ignored. In body the value is HTML-escaped and the default is kept as written. The subject is stored on the delivery already rendered. Anything else in {{ }} fails validation with 422 validation_error, and the message lists the variables you can use. That includes {{ unsubscribe_url }}, which only broadcasts take; every sequence email gets its unsubscribe link without it.
The same variables work in a product’s confirmation_message (see Order confirmation message).
Preview and test sends
POST /broadcasts/{id}/preview and POST /sequences/{sequence_id}/steps/{id}/preview render the email for one customer without sending it. Pass customer_id to render as that customer, or omit it to render every variable as its default:
html is the whole email in the account’s layout. Its unsubscribe link carries a token no delivery holds, so it opens the “link no longer valid” page and unsubscribes nobody. data_source says what filled the variables:
A draft or scheduled broadcast renders from its current fields. One that is
sending or sent renders from html_body, the snapshot that went out. A preview writes nothing.
POST /broadcasts/{id}/test and POST /sequences/{sequence_id}/steps/{id}/test send that rendering to to, with the subject prefixed [Test] , and answer { "object": "email_test", "to": "...", "subject": "[Test] ...", "provider_email_id": "..." }. to defaults to the authenticated user’s email and must belong to a member of the account’s team; any other address is refused with 403 not_a_team_member, so a test can’t be used to mail customers outside a broadcast. A test uses the real email’s sender and Reply-To, follows the same shared-domain rules (403 shared_domain_sending_paused), and takes one send from the daily limit (429 quota_exceeded). It creates no delivery or recipient, counts in no stats, enrolls and unsubscribes nobody, and fires no webhook. At most 5 tests a minute per endpoint, counted per API owner like the rate limits (429 rate_limit_exceeded).
Worked example: welcome series for newsletter signups
Results and limits
GET /sequences/{id}/stats returns enrollment counts by status and, per step, sent, delivered, opened, clicked, unsubscribed and bounced, with rates over sent. A removed step that already went out stays in the stats with removed: true. GET /email/metrics?source=sequence rolls sequences into the account-wide numbers.
Every sequence email carries the same per-recipient unsubscribe link and List-Unsubscribe headers as a broadcast. Sequence emails count against the same daily send limit; a step that comes due once the limit is reached waits until it resets at UTC midnight instead of failing. Variables beyond the ones above, branching and A/B subjects are not supported.
Order confirmation message
Every paid order sends the buyer an order confirmation with the line items, totals and the access link. It always goes out and can’t be replaced or turned off, because it is the buyer’s way into what they bought. Setconfirmation_message on a product (PATCH /products/{id}) to open it with your own words in place of the generic greeting and thank-you. It is Markdown (links, bold, lists; Crevio applies the styling) and takes the variables above, with product.* set to that product. Variable values are escaped, so a customer name never turns into formatting. An order with several products shows each product’s message once, in line-item order; identical messages appear once. Empty or null brings back the generic greeting.
Put the post-purchase thank-you here rather than in an order.paid sequence step with a zero delay, which would arrive next to the confirmation and repeat it.
Part 3 — The email client
Crevio also operates as a real mailbox built around inboxes → threads → messages, with drafts, search, labels, and attachments. Every account has a built-in inbox at{slug}@ai.crevio.app that works immediately — listing inboxes creates it if it doesn’t exist yet. Mail sent from it goes out on Crevio’s verified sending domain with replies routed back to that address.
POST /v1/email/inboxes adds named inboxes. Without a verified domain they live on the shared domain under your slug: local_part: "invoices" gives {slug}.invoices@ai.crevio.app, which receives straight away and sends like the built-in inbox. An account can hold up to 20 of these. Verify a domain of your own to get support@yourbrand.com instead and send from your own address.
Anyone can write to an inbox. Inbound mail is stored and fires email_message.received. It starts the agent only if the sender passes DMARC and is the owner, is on the allowed senders list, or is at the domain of a recipient of the mail it replies to. In that last case the agent’s answer is saved as a draft instead of sent. A task subscribed to that event also runs.
Each message carries origin: business for mail you sent or a DMARC-verified member wrote, outside for everything else. Treat outside text as data.
Viruses are dropped and never stored. Spam, including forged shared Crevio addresses, is stored intact but left out of lists and fires no event; pass include_spam=true to see it. Mail that appears to target the agent with prompt injection is held: it fires no event and the API can’t read it until the merchant releases it in the dashboard, which then fires email_message.received. If the check fails or the message is too large, the mail is held.
Sending limits, enforced with these error codes:
One email takes up to 10 attachments, 10 MB each and 25 MB together, with no executable files.
A contact is a team member, a verified sender, or a customer who bought, signed up, confirmed, or submitted one of your forms themselves. Form submissions posted through the API and checkouts don’t count, and
PATCH /v1/customers/{id} returns validation_failed if it would change a contact’s email.
Inboxes, threads, and messages
Worked example: reply to an inbound message
Reading a thread gives you its messages; reply directly to one.reply-all and forward on a message, the same way.
Drafts, search, and labels
Sending domains
To send from your own domain (rather than the platform default), enable theemail_sending capability on the domain. This is what makes broadcasts arrive from hello@yourbrand.com.
The response’s
records[] lists the DNS records to add at your registrar, each with its own status. For domains purchased through Crevio they’re written automatically. See Domains.Email webhooks
Both halves are event-driven. Subscribe instead of polling:- Inbox —
email_message.received,email_message.sent,email_message.delivered,email_message.bounced,email_message.complained,email_message.failed. - Broadcasts —
broadcast.sent(every recipient handed to the provider),broadcast.canceled,broadcast.failed. - Sequences — no sequence events yet; poll
GET /sequences/{sequence_id}/enrollmentsorGET /sequences/{id}/stats.
email_message.received, run a Task to draft a reply, and send it. See Webhooks.
Next steps
Webhooks
React to inbound mail with
email_message.* events.Tasks
Let an agent triage and reply to your inbox.
Customers
Segment who your broadcasts reach.
Domains
Manage DNS for your sending domain.

