Send email API

Send email your users can answer

One call sends from your domain or from a user's own Gmail or Outlook. Opt-outs and pacing are checked before it leaves, delivery comes back as webhooks, and a reply lands on the thread the send opened.

What your code sends.

POST /api/v2/emails201
const email = await aiinbx.emails.send(  {    from: "Yourapp Billing <billing@mail.yourapp.com>",    to: "ines@moreau.studio",    subject: "Invoice INV-2041",    html,    text,  },  { idempotencyKey: "invoice-2041" },) email.status      // "queued"email.thread_id   // "thr_5c1e"email.suppressed  // []email.pacing      // null

What comes back to your webhook.

  1. email.sent09:14:02

    Handed to the mail provider

    thread_id thr_5c1e

  2. email.delivered09:14:04

    Accepted by moreau.studio's mail server

    thread_id thr_5c1e

  3. email.received11:40:17

    “Could you add our VAT number and resend?”

    thread_id thr_5c1e

Mailboxes

From your domain, or from your user's own inbox

The address in from decides what carries the mail. A verified domain sends it signed with your key; a connected Gmail or Outlook account sends it as that person, and it sits in their Sent folder. The call is the same.

Mailboxes
from can be any of these
  • billing@mail.yourapp.comDomain

    Signed with your domain's DKIM key

  • billing@acme.yourapp.comWildcard

    One customer's subdomain of *.yourapp.com, no DNS of its own

  • nora.berg@gmail.comGmail

    Sent through her account, kept in her Sent folder

  • jonas@northwind.ioOutlook

    Sent through his account, kept in his Sent folder

  • hello@yourapp.aiinbx.appProvided

    Works before any DNS, up to 100 a day

Threading

The answer comes back to the same thread

Every send opens a thread or joins one. When the recipient replies to a connected inbox, or to a domain whose MX record points here, it arrives as email.received with the thread_id the send returned.

Threading
Invoice INV-2041thr_5c1e
billing@mail.yourapp.com → ines@moreau.studioemail.sent

Your invoice INV-2041 is attached. It is due on 10 October.

ines@moreau.studio → billing@mail.yourapp.comemail.received

Could you add our VAT number, FR40123456789, and resend?

threads.reply("thr_5c1e", { text, attachments })

To Ines, as Re: Invoice INV-2041, with In-Reply-To and References written from the thread.

Checks

Checked on the way out, told in the response

A suppressed recipient is dropped and a held message waits in a queue. Unless every address in to is suppressed, neither is an error: the response says what happened, so there is no list to check first and nothing to retry.

  • Suppressions

    Addresses that bounced, complained or opted out are dropped and listed in suppressed. Mark a newsletter unsubscribe: true and an opt-out stops that list, not the receipts you send under another key.

  • Pacing

    Caps per sender, domain and recipient hold a message in a queue rather than refusing it. pacing names the rule and when it should leave.

  • Sending hours

    Mail sent outside the hours you allow waits for the window to open. The password reset passes pacing: { skip: true } and goes at once.

Sunday, 22:40held until Monday
const email = await aiinbx.emails.send({  from: "Yourapp <reports@mail.yourapp.com>",  to: "ines@moreau.studio",  cc: "marc@moreau.studio",  subject: "Your September usage report",  html,  text,}) email.suppressed                // ["marc@moreau.studio"]email.pacing.held_by            // { kind: "hours", rule_id: "pace_…" }email.pacing.estimated_send_at  // "2026-09-28T07:00:00Z"

Webhooks

Delivery arrives as events, not a status to poll

What happens after the handoff is posted to your endpoint, signed and retried for a day if it is down. Every delivery event carries the email and thread id, so a bounce finds its customer.

Webhooks
  • email.sentHanded to the provider. Acceptance, not delivery.
  • email.deliveredThe receiving server took it, reported per recipient.
  • email.bouncedA permanent bounce suppresses the address by itself.
  • email.complainedMarked as spam. The address is suppressed on that send's list.
  • email.failedRefused before any attempt. Fix the message, not the address.
  • email.opened · clickedOnly from a domain, and only with tracking turned on.

Reference

Everything one send takes

Only from, to, subject and a body are required. The rest is there for the day you need it, on the same endpoint, and a reply takes the same options.

Sending guide
from
An address on a verified domain or a connected mailbox, with a display name if you want one.
to · cc · bcc · reply_to
One address or a list, up to 100 in each.
html · text
One or both, up to 2 million characters each. The TypeScript SDK also takes a React Email component as react.
attachments
Up to 20 files in base64, 3 MB in all. Give one a cid to show it inline.
headers
Your own headers, such as a campaign or correlation id. Message-ID and the threading headers are written for you.
Idempotency-Key
A request header. A retry with the same key and body returns the first email instead of sending a second.
suppression_key · unsubscribe
Which opt-out list the send answers to, and whether the mail is optional. Optional mail goes to one recipient per send.
pacing · tracking
Skip the rules for urgent mail; turn open and click tracking on or off for one email from a domain.
scheduled_at
Send later, up to 30 days out. It can be moved or cancelled until its time comes.
thread_id · in_reply_to
Continue a conversation, including one that began before it ever passed through AI Inbx.

Set up

Sending, in three calls

The same calls in the TypeScript and Python SDKs, or as plain HTTPS with a bearer key from any language.

1domains.createor start on your-slug.aiinbx.app
const domain = await aiinbx.domains.create({  name: "mail.yourapp.com",}) domain.records // DKIM, SPF, DMARC, return path, and MX for replies
2emails.senda key per invoice, so a retry never sends twice
await aiinbx.emails.send(  {    from: "Yourapp Billing <billing@mail.yourapp.com>",    to: invoice.customerEmail,    subject: "Invoice INV-2041",    react: <InvoiceEmail invoice={invoice} />,    text: invoiceText(invoice),  },  { idempotencyKey: "invoice-2041" },)
3your webhookbounces recorded, answers opened
const event = await verifyWebhookRequest(request, secret) if (event.type === "email.bounced" && event.data.permanent) {  await customers.markUndeliverable(event.data.recipients)}if (event.type === "email.received") {  await billing.openQuery(event.data.thread_id)}

Questions

What is a transactional email API?

An HTTP endpoint your application calls to send one message to one person because of something they did: a receipt, a password reset, an invoice, a notification. The service signs the mail for your domain, hands it to the receiving server and reports back what happened, so your app never talks SMTP or runs a mail server.

Should I send email over SMTP or an HTTP API?

AI Inbx is an HTTP API only; there is no SMTP relay. A send is one HTTPS request, which works from serverless and edge runtimes where outbound SMTP is often blocked, and it answers at once with the email's id, its thread and whether anything held or dropped it, where an SMTP relay only says it took the message.

How do I keep transactional email out of the spam folder?

Send from a domain you authenticate. Adding a domain returns its DKIM, SPF, DMARC and return-path records, and it cannot send until DKIM verifies; diagnostics reads the live zone when a record will not resolve. Suppressions then stop you mailing addresses that bounced or complained, which is what mailbox providers judge a sender on.

Can my app send email from my users' own Gmail or Outlook address?

Yes. The user connects the account once over OAuth, from a hosted page, a link or an API call, and their address works as a from like any domain address. The mail goes out through Google or Microsoft, sits in their own Sent folder, and replies come back on the same thread. Mail sent this way is not open or click tracked.

Can I send from my customers' domains?

Yes. The domain does not have to be yours: add it, show your customer the records to publish, and wait for domain.verified. To skip their DNS entirely, verify one wildcard such as *.yourapp.com and give each customer a subdomain like acme.yourapp.com, which sends straight away.

How do I know whether an email was delivered?

Subscribe to the delivery webhooks. email.delivered fires when the receiving server accepts the message, naming the recipients it covers; email.bounced, email.complained and email.failed say when it did not. Acceptance by the receiving server is as far as any email API can see, so no service can promise inbox rather than spam folder placement.

Can I still email someone who unsubscribed?

Transactional mail, yes. The unsubscribe link and the mail client's button put the address on the list named by the send's suppression_key, where it only blocks mail marked unsubscribe: true, so a receipt or password reset still arrives. Someone who chooses to stop everything, or asks in a reply, blocks all mail on that list, as a spam complaint does; a hard bounce blocks every send to that address.

How do I stop a retry from sending the same email twice?

Pass an Idempotency-Key header derived from your own data, such as invoice-2041. A retry with the same key and body returns the original email with Idempotent-Replayed: true instead of sending again, and the same key with a different body fails with idempotency_key_reused.

Give your product an inbox. The hard parts are already handled.

Get started