Skip to content
AI Inbx
Esc
navigateopen⌘Jpreview
On this page

Domains

Add a sending domain, publish the DNS records, verify it, diagnose the one record that won't resolve, and cover every subdomain at once with a wildcard.

Adding a domain makes every address on it usable for sending, and — if you publish the MX record — for receiving too. Adding one returns the DNS records to publish; verification checks that they resolve.

The domain does not have to be yours. A multi-tenant product adds a customer’s domain, shows them the returned records to publish at their registrar, and polls verify or waits for domain.verified. The only requirement is that someone can publish DNS for it. Where nobody can, connect a mailbox instead.

Your provided domain

Every workspace comes with one domain: <slug>.aiinbx.app, where <slug> is the workspace’s URL handle — like a .vercel.app domain. It works right away, every address on it sends and receives, and there is no DNS to set up. Handy for a side project, or for getting going before you bring a domain of your own.

const domains = await aiinbx.domains.list().all()
const provided = domains.find((domain) => domain.provided)

await aiinbx.emails.send({
  from: `hello@${provided.name}`,
  to: "someone@example.com",
  subject: "Sent before a single DNS record",
  text: "It works out of the box.",
})

It is listed with provided: true, has no records, and cannot be deleted — it goes when the workspace does. Renaming the workspace does not rename it. A name under aiinbx.app cannot be added by hand, and fails with reserved_domain.

Adding a domain

const domain = await aiinbx.domains.create({
  name: "example.com",
  region: "eu-central-1",
})

for (const record of domain.records ?? []) {
  console.log(
    record.purpose,
    record.type,
    record.name,
    record.value,
    record.ttl
  )
}
domain = client.domains.create(name="example.com", region="eu-central-1")

for record in domain.get("records", []):
    print(record["purpose"], record["type"], record["name"], record["value"])
curl https://api.aiinbx.com/api/v2/domains \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "example.com", "region": "eu-central-1" }'

Creating, updating, and deleting domains needs a full scope key.

Regions

region selects the regional mail infrastructure for this domain: eu-central-1 (the default) or us-east-1. It is fixed at creation — moving a domain between regions means deleting and re-adding it, and republishing DNS. Do not treat this setting alone as a guarantee that all application data, logs, or subprocessors stay in that region. Confirm your residency requirements separately before choosing a region.

Adding the same identity twice in one workspace fails with 409 domain_taken. That includes the apex and wildcard forms of one name, such as example.com and *.example.com. See Moving a domain between workspaces.

Spaces

space_id puts the domain in a space, and with it every email and thread that goes through the domain from then on. Omit it and the domain belongs to the workspace itself, space_id: null. There is no move afterwards; a space is decided when the domain is created.

The records

records lists everything to publish, each with the purpose it serves and a state of pending, verified, or missing.

Purpose Type What it does
SPF TXT Authorizes AI Inbx to send for the domain.
DKIM TXT Publishes the keys that sign your outgoing mail.
DMARC TXT Tells receivers what to do when SPF or DKIM fails, and where to send reports.
RETURN_PATH MX Routes bounces and complaints back so delivery status is accurate.
INBOUND MX Delivers mail to the domain into AI Inbx. Only needed if you want to receive.

Publish the exact names, types, and values returned by the API. Check existing records before making changes, especially an existing DKIM selector or DMARC policy. Sending requires DKIM verification; the return-path, SPF, and DMARC records configure authentication and bounce handling. Receiving separately requires the inbound MX record.

Verifying

Publishing DNS doesn’t tell us anything by itself, so ask for a check:

const domain = await aiinbx.domains.verify("dom_...")

if (domain.verified_at) {
  console.log("ready to send")
} else {
  const missing = domain.records?.filter((r) => r.state !== "verified")
  console.log(
    "still waiting on",
    missing?.map((r) => r.purpose)
  )
}
domain = client.domains.verify("dom_...")
print(domain["verified_at"])
curl -X POST https://api.aiinbx.com/api/v2/domains/dom_.../verify \
  -H "Authorization: Bearer $AI_INBX_API_KEY"

verified_at is null until the sending identity’s DKIM verification succeeds. It does not certify inbound MX, SPF, or DMARC configuration. Each record’s last_checked_at tells you when it was last looked at. Propagation is usually minutes and occasionally hours — poll with a sane interval rather than in a tight loop.

Sending from an unverified domain fails with 409 domain_unverified.

Diagnostics

When a record won’t verify and you can’t see why, diagnostics reads the live zone and reports what it finds. It’s a read-only check — nothing is stored, so it always reflects DNS as it is right now.

const { data } = await aiinbx.domains.diagnostics("dom_...")

for (const finding of data) {
  console.log(`[${finding.severity}] ${finding.title}`)
  console.log(finding.detail)
  console.log("fix:", finding.fix)
}
findings = client.domains.diagnostics("dom_...")["data"]

for finding in findings:
    print(finding["severity"], finding["title"], finding["fix"])
curl https://api.aiinbx.com/api/v2/domains/dom_.../diagnostics \
  -H "Authorization: Bearer $AI_INBX_API_KEY"

Each finding carries a severity, the purpose it concerns, a detail explaining what’s wrong, a fix describing what to change, and evidence — the actual records read from the zone.

PropType
breakingseverity

Mail will fail or be rejected. Fix before sending.

Typeseverity
risk?severity

Deliverability is likely to suffer — a too-permissive SPF policy, a DMARC record set to none.

Typeseverity
tip?severity

A hardening suggestion. Nothing is broken.

Typeseverity

Findings also carry when: adding applies while you’re setting the domain up, added to one already live, and both either way.

The classic breaking finding is two SPF records on the same name — receivers treat that as a permanent error and ignore both. Diagnostics names the offending records so you know which to merge.

Wildcards and subdomains

A wildcard — *.saas.com — is one domain that covers every subdomain. Publish its records once and anything.saas.com can send and receive, without touching DNS again. It is how a platform hands each customer an address of their own.

const wildcard = await aiinbx.domains.create({ name: "*.saas.com" })

The records go in the saas.com zone, but the MX record sits at the wildcard rather than the apex — saas.com keeps whatever mail setup it already has, and only its subdomains route to AI Inbx:

Purpose Type Name
DKIM TXT aibx._domainkey.saas.com
INBOUND MX *.saas.com
RETURN_PATH MX bounces.saas.com
SPF TXT bounces.saas.com
DMARC TXT _dmarc.saas.com

saas.com and *.saas.com are the same identity on the mail provider, so only one of the two can exist here: adding the second fails with domain_taken. Pick the apex if you send from saas.com itself; pick the wildcard if the addresses are <customer>.saas.com.

Subdomains

Once a wildcard is yours, a name one label under it — acme.saas.com under *.saas.com — is created as a subdomain of it:

const domain = await aiinbx.domains.create({
  name: "acme.saas.com",
  space_id: "spc_...",
})

console.log(domain.parent_id) // the wildcard's id
console.log(domain.records) // []
domain = client.domains.create(name="acme.saas.com", space_id="spc_...")
print(domain["parent_id"], domain["verified_at"])
curl https://api.aiinbx.com/api/v2/domains \
  -H "Authorization: Bearer $AI_INBX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "acme.saas.com", "space_id": "spc_..." }'

A subdomain has no records of its own and nothing to verify — verified_at is set the moment the wildcard’s is, and follows it. parent_id names the wildcard. region and the tracking flags are inherited. It is usually created in a space, so that the customer’s mail is theirs.

What is not a subdomain: a.b.saas.com is two labels down and is an ordinary domain with records of its own, and so is acme.saas.com in a workspace that has no *.saas.com yet.

One name is refused: bounces.saas.com is the wildcard’s return path, and fails with 409 reserved_subdomain.

Deleting

A subdomain is a row: deleting it unpublishes nothing. Deleting a wildcard deletes every subdomain filed under it, in whatever space they sit — they are names under its identity and its records, and nothing is left to publish for them. Mail already sent or received is kept either way. Remove DNS records that belonged to the deleted connection after checking that they are no longer needed.

Tracking settings

PATCH /domains/{id} toggles open and click tracking:

await aiinbx.domains.update("dom_...", {
  track_opens: false,
  track_clicks: true,
})

The values are returned on the domain as tracking.opens and tracking.clicks.

Listing and deleting

const domains = await aiinbx.domains.list().all()

await aiinbx.domains.delete("dom_...")
for domain in client.domains.iter():
    print(domain["name"], domain["verified_at"])

client.domains.delete("dom_...")

Deleting a domain stops future sends from it. Remove the DNS records afterwards — an SPF or DKIM record pointing at a domain that no longer exists is dead weight in your zone. The provided domain cannot be deleted: the call fails with domain_provided.

Knowing when it’s ready

Rather than polling, subscribe to domain.verified:

if (event.type === "domain.verified") {
  console.log(event.data.name, "is ready in", event.data.region)
}

Staying verified

Verification is not a one-off. Domains are rechecked in the background, and one whose DKIM record stops resolving loses verified_at again: sends from it fail with domain_unverified until the record is back, and domain.lost tells you it happened. Nothing else changes — the domain, its records and its mail stay where they were, and the next check that finds the key restores it.

Both transitions are also mailed to the workspace’s owners and admins, batched per workspace: one mail lists everything that verified or was lost since the last one, lost first. Domains connected inside a space are never mailed — the webhook has them. Each admin sets their own level — Everything, Problems only, or Off — under Settings → Notifications, or from the link at the bottom of any of these mails.

Moving a domain between workspaces

A domain name can be connected in multiple workspaces, but only the workspace whose DKIM key is currently published can send from it. Add the domain in the destination workspace, then publish the DNS records returned by that connection. The next verification check transfers sending to it and marks the previous connection as lost. Existing mail remains in the workspace that sent or received it.

Within one workspace, the apex and wildcard are one connection: example.com and *.example.com cannot both be added there. Where multiple workspaces need to send at the same time, give each one a distinct subdomain.

Last updated on September 12, 2026

Was this page helpful?