Email and inbox
Connecting mailboxes, sending, sending limits, deliverability, and how replies are classified and answered.
Mailboxes
ZeroReach sends from your own mailboxes. POST /api/v1/mailboxes/connect starts connecting a Gmail, Outlook or IMAP mailbox and returns the provider's hosted sign-in URL. When you finish, the mailbox appears in GET /api/v1/mailboxes. Provider secrets are encrypted at rest.
A workspace can also bring its own mail provider account; its inbound events arrive at /api/v1/webhooks/mail/{config_id}.
Sending
Email goes out in two ways:
- Sequence steps, sent by the runner.
- One-off emails to a lead, with
POST /api/v1/mailboxes/{mailbox_id}/send. This is a human-only action: Zero never sends, and with an API key it needsmailboxes:write.
Both paths enforce the same rules in code:
- The lead's email must not be suppressed, and the contact check must allow it. Otherwise:
409 Sending is not allowed: <reasons>. - The mailbox must be connected, not paused by the deliverability guard, and under its daily cap (
409). - A send held back by contact pressure or a sending limit answers
409 contact_deferredwithnext_allowed_at. - Marketing email carries one-click
List-Unsubscribeheaders and the CAN-SPAM footer with the workspace's postal address (409 missing_postal_addresswithout one). - Every send is logged and added to the lead's timeline (
email_sent).
Sending limits
Before each email, the server takes one token from every bucket that covers it. If any bucket is empty, the send is deferred, never dropped.
| Bucket | Default |
|---|---|
| Per mailbox, per hour | 20 |
| Per sending domain, per day | 300 |
| Per recipient domain, per hour | 10 |
| Emails per workspace, per minute | 30 |
| Emails per provider, per minute | 120 |
A workspace can only lower these limits. Freemail and reserved domains (such as gmail.com) don't get a sending-domain or recipient-domain bucket. Each mailbox also has a daily cap (daily_send_cap), and the server can ramp a new mailbox up to it gradually (warmup).
Deliverability
GET /api/v1/deliverability scores sender health from 0 to 100, per mailbox and per sending domain. The score covers hard-bounce and complaint rates, pauses, the connection, and the domain's last SPF, DKIM, DMARC and MX check (POST /api/v1/deliverability/domains/{domain}/check).
The deliverability guard pauses a mailbox when bounces or complaints spike, before providers penalise the domain. The rules, over a rolling 7-day window:
- a hard-bounce rate above 2% once at least 50 emails went out;
- 10 or more hard bounces, whatever the volume;
- at least 2 complaints and a complaint rate above 0.1%.
A paused mailbox sends nothing until you resume it (POST /api/v1/mailboxes/{mailbox_id}/resume). The pause also emits a mailbox.paused webhook event.
Replies and the inbox
Inbound replies are classified by the AI into one category: interested, question, objection, referral, out_of_office, not_interested, unsubscribe, bounce, wrong_person or other.
The AI only labels the reply. Every action is decided in code from the category:
| Category | What happens |
|---|---|
unsubscribe (or an angry reply) | The lead is opted out and every in-flight enrollment stops (opted_out). |
bounce | The email address is suppressed and every enrollment stops (bounced). |
out_of_office | The return date is stored; enrollments keep running but wait until the next business day after it. |
interested, question, objection, referral | The lead moves from new/contacted to replied (never automatically qualified), and the Inbox agent drafts an answer when it is confident enough. |
| any other reply | Enrollments of sequences with stop_on_reply stop (replied). |
Automatic replies (detected from the headers) are classified and logged, and nothing else happens.
Answer drafts go to the reply drafts queue (/api/v1/reply-drafts). They are never sent automatically: a signed-in person approves and sends each one. List replies with GET /api/v1/replies and a lead's replies with GET /api/v1/leads/{lead_id}/replies.
Unsubscribe links
Every marketing email carries a signed unsubscribe link (/u/{token}) and a List-Unsubscribe header. Using it records the opt-out in the consent ledger. Links already sent keep working.