HCC Docs

Email

Your code sends email from your own domain, through your own sending provider, and we carry it. We're a bridge: the domain, the provider account, what you send through it and what anyone says about it are yours. This page says which part of the work is ours and which is yours.

Using this doesn't make you compliant with anything. Commercial email in the US is regulated by CAN-SPAM, and its duties fall on the sender. That's you. Nothing here gives you consent, a good reputation or legal review.

What we do

  • Add a one-click unsubscribe to every Email, in its List-Unsubscribe headers, so mail apps can show their own button. It goes through us, so it works whatever your message says. The mail app's button unsubscribes at once. The link itself opens a page with a button, and only that button unsubscribes, because mail scanners open links. A reply to the mailto: address unsubscribes too. Doing it twice does no harm.
  • Refuse an address on your suppression list. An unsubscribe, a hard bounce or a spam complaint puts an address there. The list has no expiry, and lifting an entry keeps its history.
  • Keep one row per attempt, refusals included. The row says who sent it, to whom, and what became of it.
  • Keep test mode away from any provider. Test keys send through a simulator that never delivers anything.
  • Take in replies that come through your forward, and put each on its Thread by its headers. A bounce report marks the Email it's about as bounced.
  • Stop outbound when billing does. Unsubscribes keep working.

What's yours

  • A reason to email each person, and an honest sender, subject and message. CAN-SPAM forbids misleading headers and subject lines, and asks you to say when a message is an ad.
  • Your physical postal address, in every commercial message. We don't add a footer.
  • Opt-outs that come another way. A reply that says "please stop" revokes nothing on our side until your code or a person says so: c.unsubscribe() in .onEmailReply or an Email flow, hcc.emailSuppressions.add, or Settings.
  • Your domain's SPF, DKIM and DMARC, and your provider account's standing. Whether a message lands in an inbox is between you, your provider and the mailbox provider.
  • Lifting a suppression. You can remove an unsubscribe, and the law says you may not mail that person again. That's yours to justify.

Connecting Resend

Live Emails go out on your own Resend account, with your API key. Test mode never reaches it.

  1. Verify your domain in Resend. Add the domain and the DNS records Resend shows. That's between you and Resend: we only read the status.
  2. Add the address, in Settings → Email or with hcc.emailAddresses.create({ address, provider: 'resend', credentials: { apiKey } }). It's ready once Resend says the domain is verified, and unverified or failed until then, with Resend's reason. An address that isn't ready doesn't send. "Check again", or hcc.emailAddresses.check(id), reads the status now, and we read it again every day.
  3. Add the webhook. Each address shows a webhook URL (webhook.url). In Resend, add it as a webhook for every email event, then save the signing secret Resend shows as credentials.webhookSecret, in Settings → Email or with hcc.emailAddresses.update(id, { credentials: { webhookSecret } }). One webhook per Resend account is enough: any of its addresses' URLs will do.

A full-access key lets us read the domain's status. A sending-access key can't: the address then says so and is ready, and Resend itself refuses a send from a domain it hasn't verified.

What happens to a live Email:

  • We send it once, with our Message-ID, the threading headers and the one-click unsubscribe, and your Email's id as Resend's idempotency key. A send Resend gives no clear answer to stays unknown.
  • Resend's webhooks move it on: sent, delivered, bounced, complained or failed. Each is checked against the secret, and a repeat changes nothing. A permanent bounce or a complaint suppresses the address. A temporary bounce doesn't.
  • We ask Resend about anything still unsettled after a few minutes, an unknown included, and never send it again. An unknown Resend never had is failed after three days.
  • Opens and clicks are ignored, as is everything else Resend reports that we don't track.

Each Email row, in or out, sent or refused, counts as one Email on your bill, and the bytes we keep for an Email count as storage until your retention period clears them.

Receiving replies

You send from your own domain, so replies go to your own mailbox first. To get them here, forward that address's mail to its forwarding address. Each Email address has its own, shown in Settings → Email and as inboundAddress in the API.

  1. Copy the forwarding address. Keep it private. Anything sent to it lands in your workspace as mail to that address.
  2. Add a forward in your mail host. Google Workspace, Microsoft 365 or any other host works. Forward, or keep a copy and forward, to the forwarding address. Leave the headers alone, because replies find their Thread by In-Reply-To and References.
  3. Send a test. In Settings → Email, "Send test" emails the address from itself and waits for it to come back. "Received" means replies will reach you.

If the forwarding address leaks, rotate it. Mail to the old one is dropped from then on, so update the forward and send another test.

How we handle what arrives:

  • A reply joins its Thread. A message with no threading headers starts a new Thread, even when the subject matches. We never guess from the subject.
  • Each message is kept once. The same message delivered twice is one Email. A message without a Message-ID gets one made from its content.
  • Auto-replies are kept and marked. An out-of-office has autoReply: true and doesn't reopen a closed Thread.
  • Bounce reports apply to the Email they report on. A permanent failure suppresses the address, and a temporary one doesn't. A report that matches none of your Emails is kept as an auto-reply.
  • Mail from a suppressed address still arrives. Suppression stops what you send, not what you receive.
  • We refuse messages over 25 MiB, so the sender gets a bounce. Mail to a forwarding address we don't know is dropped.
  • We keep the raw message, the HTML and the attachments until your retention period ends. Then they're deleted, and the row stays.

Email in your code

Campaigns and inbound Email run in your code, as calls and Texts do. Both are in the SDK: Emails in a campaign and Email flows.

  • An .email step sends each lead one Email per step per run, written by your handler. It goes through the same checks as any other: a suppressed address takes the lead out as suppressed, and a lead with no email address skips the step. It sends from the address you name, else your oldest one that can send. With none that can, the lead parks until you fix it.
  • A reply to a campaign's Email, found by its headers, reaches your goals and exits, then .onEmailReply. With no .onEmailReply, a person answering ends the sequence. An out-of-office reaches goals and exits only, and ends nothing.
  • An Email flow runs your code for each Email that comes in to an address: assign the Thread, label it with a queue, reply in it, or suppress the sender. hcc deploy --live binds it to its addresses, and Settings → Email shows which flow each address runs. It never answers an automatic reply, and an unsubscribe never reaches it.

What a campaign says, and whether it should go at all, are still yours. We add the unsubscribe, not the postal address or the reason to write.

What we don't do

  • We don't track opens or clicks. We add no pixel and rewrite no link, so there are no open or click events.
  • We don't write a text part for you. If you send only HTML, the Email has only HTML.
  • We don't send again on our own. A send with no clear answer from the provider stays unknown, as the provider may have sent it.

Where it stands

Test mode works through the simulator, and live mode on your own Resend account. Replies through a forward work in both modes, and hcc.simulator.email plays one in test. Campaigns can send Email, and Email flows answer it. Other providers come later: creating a live address on one answers provider_unavailable. See Sending Emails for the SDK.