← Blog
LaunchApril 16, 2026Updated September 7, 20268 min read

Introducing Domains: DNS, email and apps for your agent's identity

Every organization gets a working system domain at signup. Connect a domain you already own, verify it, and hang inboxes and apps off it through the Vetta API, SDK and CLI.

Dennis Zax· CTO, Naïve

TL;DR

  • A domain is the foundation an identity's inbox sits on and the anchor an app is served from. It belongs to the organization, and several personas can send from it.
  • Three registrar kinds: system (auto-provisioned under Vetta's shared apex at signup), external (bring your own, verified through the DNS records we hand back), and purchased (bought through Vetta, approval-gated by policy).
  • Verification is not one boolean. dns_status gates sending, inbound_status gates receiving, app_connect_status gates HTTP. Each track advances on its own.
  • The API is six routes under /v1/domains, mirrored by client.domains in the SDK and vetta identity domain in the CLI. Verify is re-runnable.
  • Records the platform owns (DKIM, _dmarc, inbound MX) are readable but never editable by the agent, so it cannot break its own deliverability.

An agent needs somewhere to be reached

An agent that runs unattended needs a hostname. Its inbox needs one. The app it ships needs one. Its mail needs SPF, DKIM and MX records a receiving server will trust. Doing that by hand across registrars and DNS panels is slow, easy to get wrong, and puts a person back in the loop every time an agent needs a new address.

Domains is the Identity primitive that removes that step. It is documented at vetta.sh/docs/identity/domains and sits on the Domains page of the catalog. This post covers the three registrar kinds, the three verification tracks, and the exact API, SDK and CLI shapes you drive them with.

A domain belongs to the organization

A domain is an organization object, not an identity one. Two personas routinely send from the same domain, and the system domain is minted for the organization as a whole. An identity reaches a domain through the inboxes provisioned on it, which is why the routes live at /v1/domains rather than under an identity. Domains are shared infrastructure; inboxes and phone numbers are per persona; the policy layer decides which of them an agent may reach for at all.

Three registrar kinds

A domain's kind records where it lives and who owns it.

KindWho owns itHow it gets there
systemVettaAuto-provisioned at signup as a subdomain under Vetta's shared apex, named after your organization slug.
externalYou, at your registrarBring your own domain. Vetta returns the DNS records to add, then verifies them.
purchasedVetta registers on your behalfBought through Vetta and approval-gated by policy by default.

System domains: a working domain before you do anything

Every organization gets a system domain automatically. Provisioning is fire-and-forget: Vetta creates the domain at the email layer with sending and receiving enabled, writes the SPF, DKIM, MX and DMARC records straight into its own shared apex zone, triggers verification, and polls briefly. A background sweep re-verifies any domain still pending, so a transient propagation delay resolves itself.

The system domain is already there
vetta identity domain list
# NAME                          KIND     STATUS   DNS_STATUS    APP_CONNECT
# acme.<vetta-apex>             system   active   provisioned   pending

Because Vetta controls the zone there is no registrar step, which makes the system subdomain the fastest way to get an agent sending and receiving mail. Its zone is list-only: Vetta manages the records and any attempt to edit them returns 403.

External domains: bring your own

Connecting a domain you already own is a two-call flow. connect returns the DNS records to publish at your registrar; verify checks them and advances the status tracks.

Connect, publish, verify
vetta identity domain connect --domain acme-mail.com --kind external --receive
vetta identity domain records --domain dom_...     # publish these, then:
vetta identity domain verify --domain dom_...

The record set is returned dynamically: typically a TXT for SPF, a TXT or CNAME for DKIM, an MX for inbound, sometimes an additional CNAME, plus a _dmarc TXT. Add every returned record exactly as given. Propagation takes minutes to hours depending on the registrar, which is why verify is idempotent: run it as often as you like until the tracks clear.

Purchased domains

purchased is a domain bought through Vetta, where Vetta owns the registration and manages the zone for you. Purchasing requires human approval out of the box, alongside connecting a new third-party app and provisioning a phone number: the session emits a confirmation event and holds at storage cost until you respond.

One honest caveat. The API accepts kind: "purchased" in its schema, but today it answers 501 feature_not_configured because there is no outbound payment path to a registrar. The kind, the policy gate and the zone semantics are specified; the checkout is not wired. Ship on system or external for now.

Verification is three tracks, not one boolean

A domain advances along three independent tracks, and each gates a different capability.

FieldGatesValues
dns_statusSending mail (SPF/DKIM)pending_verification, provisioned, failed
inbound_statusReceiving mail (MX)pending, verified, failed
app_connect_statusServing an app over HTTPpending, connected, failed

The domain record also carries an overall status (pending_dns, active, failed); creating an inbox requires active.

A domain can be fully verified for email while its app track is still pending. It can be sending (dns_status: provisioned) while inbound is failed. Calling verify after publishing only the SPF and DKIM records moves dns_status and leaves inbound_status where it was; an unpublished track is not an error, it simply stays pending. Read all three fields before calling a domain healthy.

The email readiness gate

Inboxes cannot exist on an unverified domain. The rule in practice: a domain must be verified before an inbox on it can send.

ActionRequires
Create an inboxstatus: active
Send from the inboxdns_status: provisioned
Receive at the inboxverified for inbound at the email layer

A send attempted before the domain finishes verifying is not a hard failure. It returns a retryable job_not_ready, and the durable runtime retries once verification completes rather than dropping the message.

Once the domain is active, an inbox is one call away. The address is localpart@domain, and every send from it is credit-checked against the budget.

An inbox on the verified domain
vetta identity email provision --identity idn_... --address ava@acme-mail.com --domain dom_...

The full inbox flow, including auto-derived localparts and the inbound wake, is on the Mail primitive page and in the email docs.

Protected records: the agent cannot break its own mail

Some records exist to keep deliverability and inbound routing intact, and the agent is not allowed to touch them:

  • _dmarc
  • _domainkey and *._domainkey (DKIM)
  • the inbound MX and its associated TXT

In the API these come back with managed: true. An agent may read a managed record and must never edit or delete it, because removing the DKIM record breaks the signature on its own outbound mail. On a system domain the entire zone is list-only. On external and purchased domains you manage your other records, but the protected set stays off-limits. Reading the zone is always allowed on every kind: reading is never the dangerous half.

The API, in six routes

The whole surface is small enough to list, documented at vetta.sh/docs/api/domains.

RouteScopeWhat it does
POST /v1/domainsagents:writeConnect a domain. Idempotent on name.
GET /v1/domainsagents:readCursor-paginated list.
GET /v1/domains/{id}agents:readOne domain with all three tracks.
POST /v1/domains/{id}/verifyagents:writeRe-check DNS; returns the domain object.
GET /v1/domains/{id}/recordsagents:readThe DNS records to publish, with managed flags.
DELETE /v1/domains/{id}agents:writeRemove the domain. Inboxes on it stop delivering.

POST /v1/domains takes kind (defaults to external), name (required for external, omitted for system so the API mints the subdomain), and receive (also request inbound MX records; defaults to true). Connecting a domain that already exists returns the existing record, which matters when an agent retries.

Connect and verify over REST
curl -fsSL https://api.vetta.sh/v1/domains \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "kind": "external", "name": "mail.acme.dev", "receive": true }'
 
curl -fsSL -X POST https://api.vetta.sh/v1/domains/dom_01H9DM.../verify \
  -H "authorization: Bearer sk_live_..."

The same six calls in TypeScript

The TypeScript SDK mirrors the routes one to one under client.domains. It is deliberately thin: one page per list call, no retry policy of its own, and the same Idempotency-Key on every mutating verb that the CLI and dashboard send.

Connect, read the records, verify
import { randomUUID } from "node:crypto";
import { createClient } from "@usenaive-sdk/vetta";
 
const client = createClient({
  baseUrl: "https://api.vetta.sh",
  apiKey: process.env.VETTA_API_KEY!,
  fetch: globalThis.fetch,
  idempotencyKey: () => randomUUID(),
});
 
const domain = await client.domains.create({ name: "acme-mail.com", kind: "external" });
// domain.records -> the DNS records to add at your registrar
 
const { data: records } = await client.domains.records(domain.id);
 
// re-runnable; each track advances on its own
const verified = await client.domains.verify(domain.id);
console.log(verified.dns_status, verified.inbound_status, verified.app_connect_status);

On the command line the same six calls are vetta identity domain connect, list, show, verify, records and rm, documented at vetta.sh/docs/cli/comms. Because domains are org-level, this sub-group has no --identity flag.

Apps hang off a domain too

Email is the obvious consumer of a domain, but the third track exists for a reason. When an agent ships an app, the hostname it serves from is one of your org domains. Connecting a domain to an app is pointing, never registering: the domain must already exist at /v1/domains, and its app_connect_status advances pending → connected as the hosting verifies it.

Point an existing domain at an app
await client.apps.connectDomain(app.id, domain.id);
// later: client.apps.domains(app.id) lists what is connected

The REST shape is POST /v1/apps/{id}/domains with a domain_id body, GET /v1/apps/{id}/domains to list, and DELETE /v1/apps/{id}/domains/{domain} by name. In a naive.config.ts project, naive up connects an app's domains in the same reconcile that writes its secrets and ships its deploy_dir.

Where this shows up in a real deployment

A deployment runs an agent on a cron with a per-run budget, and can name the persona each fire speaks as. Give that persona an inbox on a verified domain and the scheduled agent is reachable with no person in the loop.

A nightly run that speaks as a persona with an inbox
vetta deploy create --agent nightly-triage \
  --cron "0 9 * * *" \
  --budget-usd 5 \
  --window loose \
  --prompt "Summarize what changed in main overnight." \
  --identity ava-sales \
  --on-idle https://acme.dev/hooks/vetta

Every fire is a normal session with the same events, files and budget enforcement. Without --identity a fire runs as no persona, which is rarely what you want for an agent that owns an inbox. The domain is what makes that persona addressable in the first place.

How this composes with the rest of Identity

KYC verifies the founder, LLC gives the organization a real company, and Domains gives that company a hostname. On top of it, Mail provisions inboxes, Phone provisions numbers, and Profile is the persona that speaks through them, each gated by the identity's policy.

Read the Domains docs for the full readiness tables, the API reference for every field, and Introducing Vault for the credential store that pairs with a verified identity.

FAQ

What is the Domains primitive?
Domains is the Identity primitive that gives an organization a real hostname. Vetta provisions a system subdomain at signup, lets you connect a domain you own, and tracks whether that domain can send mail, receive mail and serve an app. Inboxes and app hostnames are then created on top of it.
Do I need to buy a domain to get started?
No. Every organization gets a system domain automatically, created with sending and receiving enabled and its SPF, DKIM, MX and DMARC records written into Vetta's shared apex zone. You can provision an inbox on it without touching a registrar.
Can I bring a domain I already own?
Yes. Create the domain with kind external, add the DNS records the API returns at your registrar, then call verify. Verify is idempotent, so you can run it as often as you like while DNS propagates.
Can an agent edit my DNS?
Reading the zone is always allowed on every kind. On a system domain the whole zone is list-only. On external and purchased domains the protected set (DKIM, _dmarc and the inbound MX with its TXT) is off-limits to the agent; those records are marked managed: true in the API.
Can Vetta buy the domain for me?
Purchased is a declared registrar kind and domain purchase is approval-gated by policy by default. The current API accepts kind purchased in its schema but answers 501 feature_not_configured, because there is no outbound payment path to a registrar yet. Use a system or external domain today.