HCC Docs

Sending Emails

Your code sends email from your own domain, through your own sending provider, and we carry it. Whether you may email someone is yours to decide. We record what you send, and refuse a short list of things.

Using this doesn't make you compliant with anything. Commercial email is regulated, and CAN-SPAM's duties are the sender's: yours. Nothing here does them for you. The Email guide has the full division.

We always doYours to do
Add a one-click unsubscribe to every EmailHave a reason to email someone, and an honest subject and sender
Refuse an address on your suppression listPut your physical postal address in what you send
Suppress an address that hard-bounces or marks you as spamYour domain's SPF, DKIM and DMARC, and your sending reputation
Keep one row per attempt, refusals includedHonor opt-outs that come any other way, such as a reply
Keep test mode away from any providerWhat the messages say

We don't track opens or clicks, and we don't rewrite your links.

const from = await hcc.emailAddresses.create({
  address: 'sales@acme.com',
  displayName: 'Acme Sales',
});

const email = await hcc.emails.send({
  from: from.id,
  to: 'ana@example.com',
  subject: 'Your order shipped',
  text: 'It left our warehouse today.',
  html: '<p>It left our warehouse today.</p>',
});
email.status; // 'delivered' on the simulator; on Resend, 'accepted' or 'unknown'

const full = await hcc.emails.get(email.id); // With `text`, `html` and `attachments`
const raw = await hcc.emails.raw(email.id); // The message as it went, headers and all
for await (const each of hcc.emails.list({ threadId: email.threadId! })) each.subject;

to is one address, and cc takes more, up to 50 in all. Give text, html or both: we never make up a text part you didn't write. replyTo names an Email this one answers: it joins that Thread, with its threading headers. Attachments are base64, and the whole message is at most 25 MiB.

A key needs emails:write to send and emails:read to read, and a developer's live key can't have emails:write. Test mode sends through the simulator. Live mode sends on your own Resend account: create the address with provider: 'resend' and credentials: { apiKey }, add its webhook.url in Resend, and save the signing secret with hcc.emailAddresses.update(id, { credentials: { webhookSecret } }). hcc.emailAddresses.check(id) reads the domain's status again. The Email guide has the steps.

Refusals

A refused Email throws, and its row stays. error.details.emailId names it.

error.codeStatusWhy
invalid_recipient400An address isn't one
address_not_found404from names no Email address of yours
suppressed422A recipient is on your suppression list
address_not_ready422Your provider doesn't say the address can send
provider_unavailable422The address's provider can't carry it
too_many_recipients422Over 50 in to and cc
too_large413Over 25 MiB
billing_required, billing_past_due, billing_disputed, usage_limit_reached, plan_has_no_seats402The billing stop
test_cap_reached429This month's Test limit is used up

A send with no clear answer stays unknown and is never sent again by us. hcc.emails.send sends an Idempotency-Key; pass your own idempotencyKey in the params to make a retry safe for as long as the row exists.

Suppressions

An unsubscribe, a hard bounce or a spam complaint adds the address to the suppression list, in the same transaction as the Email's new status. A soft bounce doesn't. The list is per environment, has no expiry, and keeps its history when you lift an entry.

await hcc.emailSuppressions.add('ana@example.com', { note: 'Asked on the phone' });
await hcc.emailSuppressions.check('ana@example.com'); // The entry, or null
await hcc.emailSuppressions.remove('ana@example.com');

Lifting an unsubscribe is yours to justify: CAN-SPAM forbids mailing someone who opted out.

Email events

email.sent, email.delivered, email.bounced, email.complained, email.failed, email.refused, email.received and email.unsubscribed. They name the Email, its Thread, its addresses and its Lead, and none carries a subject or a body: read those with hcc.emails.get. email.bounced says whether it suppressed the address.

Testing Emails

Test mode never reaches a provider. These addresses answer as the name says, and any other is delivered:

ToWhat happens
bounce@simulator.hcc.runA hard bounce: email.bounced, and suppressed
soft-bounce@simulator.hcc.runA soft bounce: email.bounced, not suppressed
complaint@simulator.hcc.runDelivered, then email.complained, suppressed
reject@simulator.hcc.runThe provider refuses it: email.failed

Someone answering is hcc.simulator.email. It arrives through the address's forward, exactly as forwarded mail does, and a reply joins its Thread by its headers:

const { outcome, email } = await hcc.simulator.email({
  to: 'ema_123', // one of your test Email addresses
  from: 'ana@example.com',
  subject: 'Re: Your quote',
  text: 'Sounds good.',
  inReplyTo: sent.id, // an Email's id, or any Message-ID
});
// Or a whole message: hcc.simulator.email({ to: 'ema_123', raw: base64Eml })

outcome is received, duplicate (its Message-ID was already here), bounce (a delivery status notification, applied to the Email it reports on) or forward_verified (one of your own Emails came back through the forward). An automatic answer, such as an out-of-office, is kept with autoReply: true and doesn't reopen a closed Thread.