Mailzero documentation
The API behind Today: connect an inbox, read the brief and the facts, hand the kinds of matters you choose to your agents under standing delegations, and let them act within grants with a receipt for every effect. Domains, DNS and the rest of the platform keep their reference further down.
Support: support@mailzero.run
Authentication
All authenticated endpoints require a Bearer token in the Authorization header.
curl https://domani.run/api/domains/search?q=example.com \ -H "Authorization: Bearer domani_sk_xxx"
Get an API key by creating an account or from the Settings. Some endpoints (search, TLDs, WHOIS) are public but have lower rate limits without auth.
Token Scopes
Tokens can be restricted to specific permission scopes. Use * for full access (default). Create scoped tokens via `POST /api/tokens` with a `scopes` array.
| Scope | Grants access to |
|---|---|
| domains:read | GET /api/domains, GET /api/domains/{domain}, GET /api/domains/{domain}/dns, /dnssec, /status, /email/check, /auth-code, /transfer-away, /transfer-status, /analytics |
| domains:write | PUT /api/domains/{domain}/dns, POST/DELETE /api/domains/{domain}/dnssec, POST /connect, POST /verify, PUT /settings, PUT /parking, PUT/DELETE /api/domains/{domain}/for-sale, POST /api/domains/import, POST /import/verify |
| domains:transfer | POST /api/domains/buy, POST /transfer, POST /renew (involves payment, includes marketplace purchases) |
| tokens:read | GET /api/tokens |
| tokens:write | POST /api/tokens, DELETE /api/tokens/{id} |
| webhooks:read | GET /api/webhooks, GET /api/webhooks/{id}/deliveries |
| webhooks:write | POST /api/webhooks, PATCH /api/webhooks/{id}, DELETE /api/webhooks/{id} |
| email:read | GET /api/emails, /api/emails/{address}, /api/emails/{address}/messages, /api/emails/{address}/aliases, /api/email/changes, /api/domains/{domain}/email/status, /api/domains/{domain}/email/deliverability, /api/suppressions, GET /api/contacts |
| email:write | POST /api/emails, POST /api/emails/{address}/send, POST /api/domains/{domain}/email/setup, aliases + catch-all, POST/DELETE /api/suppressions, POST/DELETE /api/contacts |
| email:delete | Permanently delete messages already in Trash. Moving messages to Trash only requires email:write. Grant this scope only to agents allowed to irreversibly erase email content |
| email:manage_mailboxes | Create mailboxes (POST /api/emails) and nothing else. The narrow admin scope for agents that may spin up addresses for new projects (partners@, launch@) without being able to read or send mail, manage domains, or touch tokens |
| email:auth_secrets | Read messages classified as authentication mail (OTP / verification codes, password resets, magic links). Without it, email:read still lists them but subject and body come back redacted - so a stolen agent token can't harvest 2FA codes. Grant it only to agents that genuinely need to complete logins |
| account:read | GET /api/me, GET /api/agents/identity |
| account:write | DELETE /api/me, POST /api/billing/setup, POST /api/billing/subscribe, POST /api/billing/cancel, POST /api/me/resend-verification, POST/PATCH/DELETE /api/agents/identity |
| billing:read | GET /api/billing/invoices |
| search | GET /api/domains/search, /suggest, /whois, /dns-check, GET /api/tlds |
| deals:read | GET /api/deals, GET /api/deals/{id} |
| deals:write | POST /api/domains/sell, PATCH /api/deals/{id} |
| notifications:read | GET /api/notifications, GET /api/notifications/count |
| backorders:read | GET /api/backorders, GET /api/backorders/{id} |
| backorders:write | POST /api/backorders, DELETE /api/backorders/{id} |
| assistant:read | Read Mailzero assistant settings, the Today queue, Work items, exact action previews, history-import progress, and content-minimized Activity |
| assistant:write | Change Mailzero assistant settings, record consent and start history imports, record human Decisions on Work items, retry imports, and export or delete derived assistant data. Never sends mail |
Scope attenuation: a token can only create child tokens with equal or fewer scopes - never more.
Missing scope returns 403 with code `INSUFFICIENT_SCOPE` and a hint naming the required scope.
Agent quickstart: the task loop
Give an agent an identity and a token, hand it a kind of matter with a standing delegation at observe, then let it read the envelope of each matter, ask for effects and report. The level and the grants approve, escalate to you or refuse; every effect leaves a receipt; raise the level when the receipts earn it.
# The owner, once
curl -X POST https://domani.run/api/agents/identity -H "Authorization: Bearer $OWNER_KEY" -H "Content-Type: application/json" -d '{"slug":"finance-helper","name":"Finance helper"}'
curl -X POST https://domani.run/api/tokens -H "Authorization: Bearer $OWNER_KEY" -H "Content-Type: application/json" -d '{"name":"finance helper","scopes":["assistant:read","assistant:write"],"agent_identity_id":"agt_123"}'
curl -X POST https://domani.run/api/assistant/rules -H "Authorization: Bearer $OWNER_KEY" -H "Content-Type: application/json" -d '{"name":"Acme","match":{"senderDomain":"acme.com"},"assignee_type":"agent","assignee_ref":"finance-helper","level":"observe"}'
# The agent, for every matter
curl https://domani.run/api/assistant/today -H "Authorization: Bearer $AGENT_KEY"
curl https://domani.run/api/assistant/tasks/wi_789 -H "Authorization: Bearer $AGENT_KEY"
curl -X POST https://domani.run/api/assistant/tasks/wi_789/effects -H "Authorization: Bearer $AGENT_KEY" -H "Content-Type: application/json" -d '{"effects":[{"kind":"label","mailbox_id":"mbx_123","add":["finance"]}],"idempotency_key":"label-wi_789-1"}'
curl -X POST https://domani.run/api/assistant/tasks/wi_789/report -H "Authorization: Bearer $AGENT_KEY" -H "Content-Type: application/json" -d '{"outcome":"done","summary":"Labelled finance; the invoice matches the order."}'Mailzero Assistant
The Mailzero assistant endpoints turn a user's own mail into grounded Decisions: a versioned Today queue, one Work item per thread that needs judgment, and an exact Action plan prepared only after the user chooses. Nothing under /api/assistant sends, deletes, or changes mail; the model output is always a proposal that the server validates.
Two scopes cover it: assistant:read for settings, Today, Work items, Action plans, import progress, and Activity; assistant:write for settings changes, consent and history imports, human Decisions on Work items, and derived-data export or deletion.
Processing starts only after explicit consent: POST /api/assistant/preview records the consent version, scopes the owned mailboxes, and starts a resumable background import. Poll GET /api/assistant/backfill for progress.
curl -X POST https://domani.run/api/assistant/work-items/wi_789/interactions -H "Authorization: Bearer domani_sk_xxx" -H "Content-Type: application/json" -d '{"type":"choose","work_item_version":3,"decision_id":"dec_1","decision_version":1,"option_id":"opt_send","idempotency_key":"choose-wi_789-opt_send-1"}'A choice prepares an exact plan. Read it with GET /api/assistant/action-plans/{id}, show it to the user, then send through the normal reply endpoint POST /api/emails/{address}/messages/{id}/reply with assistant_plan_id. The server refuses stale or mismatched plans, records one receipt per send, and moves the Work item to Waiting or Handled.
Authenticated mailbox webhooks
In the dashboard, open a domain's Email tab, choose a mailbox, then select Authorization or X-API-Key. Domani encrypts the value and sends it with webhook tests and inbound email deliveries. Live inbound deliveries are durable and retried up to three times; `X-Domani-Delivery-Attempt`.
read -rsp 'Sender credential: ' WEBHOOK_SENDER_AUTH; echo export WEBHOOK_SENDER_AUTH jq -n --arg url 'https://agent.example/webhook' --arg auth "$WEBHOOK_SENDER_AUTH" '{url:$url,headers:'{Authorization:$auth}} | \ curl --fail-with-body -X PUT "https://domani.run/api/emails/agent%40example.com/webhook" \ -H "Authorization: Bearer $DOMANI_API_KEY" \ -H "Content-Type: application/json" --data-binary @- unset WEBHOOK_SENDER_AUTHHeader values are never returned. Responses expose only `header_names`. Omit `headers` to preserve the stored value, send an empty object to clear it, and use `POST /api/emails/`'{address}'`/webhook/test` to verify delivery.
Starting from scratch? Include `webhook_headers` in `POST /api/agents/provision` to buy the domain, create its mailbox, and attach the authenticated event webhook in one idempotent call. Domani validates the URL and header names before any charge.
Errors
All errors return a consistent JSON envelope. Use `code` for programmatic handling and `hint` for recovery guidance.
{
"error": "Missing API key",
"code": "MISSING_API_KEY",
"hint": "Include 'Authorization: Bearer domani_sk_xxx' header",
"documentation_url": "https://domani.run/llms.txt"
}| Code | HTTP |
|---|---|
| MISSING_API_KEY | 401 |
| INVALID_API_KEY | 401 |
| EXPIRED_API_KEY | 401 |
| MISSING_PARAMETER | 400 |
| INVALID_DOMAIN | 400 |
| PAYMENT_REQUIRED | 402 |
| DOMAIN_UNAVAILABLE | 409 |
| RATE_LIMIT_EXCEEDED | 429 |
| USER_EXISTS | 409 |
| NOT_FOUND | 404 |
| UNKNOWN_PROVIDER | 400 |
| UNKNOWN_METHOD | 400 |
| TARGET_REQUIRED | 400 |
| INVALID_PARAMETER | 400 |
| CONTACT_REQUIRED | 400 |
| INVALID_CONTACT | 400 |
| INSUFFICIENT_SCOPE | 403 |
| SCOPE_ESCALATION | 400 |
| MAILBOX_CONFINED | 400 |
| DOMAIN_EXISTS | 400 |
| DOMAIN_NOT_ACTIVE | 400 |
| UNSUPPORTED_TLD | 400 |
| TLD_NOT_IN_PUBLIC_DNS | 400 |
| NOT_AVAILABLE | 400 |
| ALREADY_REGISTERED | 400 |
| TRANSFER_NOT_ELIGIBLE | 400 |
| RENEWAL_NOT_ELIGIBLE | 400 |
| ALREADY_LISTED | 400 |
| LISTING_SOLD | 400 |
| SELF_PURCHASE | 400 |
| DESCRIPTION_TOO_LONG | 400 |
| DEAL_NOT_FOUND | 400 |
| INVALID_DEAL_STATE | 400 |
| EPP_ATTEMPTS_EXCEEDED | 400 |
| ACTIVE_DEAL_EXISTS | 400 |
| AGENT_IDENTITY_NOT_FOUND | 400 |
| AGENT_IDENTITY_ESCALATION | 400 |
| AGENT_MAILBOXES_REQUIRED | 400 |
| CREDENTIAL_REVOKE_FAILED | 400 |
| INVALID_IDEMPOTENCY_KEY | 400 |
| IDEMPOTENCY_KEY_MISMATCH | 400 |
| IDEMPOTENCY_KEY_REUSED | 400 |
| OPERATION_IN_PROGRESS | 400 |
| OPERATION_SUPERSEDED | 400 |
| INVALID_OPERATION_RECEIPT | 400 |
| WORKSPACE_ADMIN_REQUIRED | 400 |
| WORKSPACE_OWNER_REQUIRED | 400 |
| PRINCIPAL_NOT_IN_WORKSPACE | 400 |
| MAILBOX_NOT_IN_WORKSPACE | 400 |
| GROUP_NOT_IN_WORKSPACE | 400 |
| COLLECTION_NOT_IN_WORKSPACE | 400 |
| ACCESS_VERSION_CONFLICT | 400 |
| ACCESS_OFFER_INVALID | 400 |
| ACCESS_OFFER_EMAIL_MISMATCH | 400 |
| MAIL_DOMAIN_CAPACITY_EXHAUSTED | 400 |
| MAIL_PROVIDER_ERROR | 400 |
| INVALID_HTTP_ROUTE_HOSTNAME | 400 |
| INVALID_HTTP_ROUTE_UPSTREAM | 400 |
| INVALID_HTTP_ROUTE_UPSTREAM_HEADERS | 400 |
| INVALID_HTTP_ROUTE_LEASE | 400 |
| INVALID_HTTP_ROUTE_POLICY | 400 |
| INVALID_HOSTNAME_BINDING | 400 |
| HTTP_ROUTE_HOSTNAME_NOT_ALLOWED | 400 |
| HTTP_ROUTE_HOSTNAME_REQUIRED | 400 |
| HTTP_ROUTE_HOSTNAME_RESERVED | 400 |
| HTTP_ROUTE_HOSTNAME_UNAVAILABLE | 400 |
| HTTP_ROUTE_CONCURRENCY_LIMIT | 400 |
| HTTP_ROUTE_WORKSPACE_LIMIT | 400 |
| HTTP_ROUTE_DOMAIN_NOT_OWNED | 400 |
| HTTP_ROUTE_TERMINAL | 400 |
| HTTP_ROUTE_VERIFICATION_NOT_ENABLED | 400 |
| HTTP_ROUTE_VERIFICATION_REQUIRES_HTTPS | 400 |
| HTTP_ROUTE_POLICY_WITHOUT_SCOPE | 400 |
| HTTP_ROUTE_POLICY_REQUIRED | 400 |
| HTTP_ROUTE_AGENT_IDENTITY_REQUIRED | 400 |
| HTTP_ROUTE_POLICY_ESCALATION | 400 |
| HOSTNAME_BINDING_NOT_ACTIVE | 400 |
| HOSTNAME_BINDING_DNS_CONFLICT | 400 |
| HOSTNAME_BINDING_ACK_REQUIRED | 400 |
| HOSTNAME_BINDING_BROWSER_MODE_MISMATCH | 400 |
| HOSTNAME_DNS_CONFLICT | 400 |
| ZONE_READ_FAILED | 400 |
| DNS_VERSION_CONFLICT | 400 |
| UNAUTHORIZED | 400 |
| APEX_WEB_MODE_CONFLICT | 400 |
| APEX_DELEGATION_ACK_REQUIRED | 400 |
| VERIFICATION_COOLDOWN | 400 |
| VERIFICATION_SEND_FAILED | 400 |
| UPGRADE_REQUIRED | 400 |
| FREE_LIMIT_EXCEEDED | 400 |
Rate Limits
Every response includes rate limit headers:
`X-RateLimit-Limit`- max requests per window`X-RateLimit-Remaining`- requests left`X-RateLimit-Reset`- unix timestamp when window resets
When rate limited (HTTP 429), check the `Retry-After` header for seconds to wait.
Mailzero Assistant
/api/assistant/rulesList the standing delegations
The owner's standing delegation rules in order: name, the match predicate, the assignee (an agent identity or a workspace member), the autonomy level, how often each matched. Requires assistant:read; an agent token is refused.
Response 200
| rules | AssistantRule[] |
/api/assistant/rulesCreate a standing delegation
Hand a kind of matter to an agent or a person once. match is an explicit predicate, never a model call: any of senderAddress, senderDomain, organisation, workKeyPrefix, senderKind, mailboxIds, labels, subjectPattern (anchored, case-insensitive), minOccurrences, minConfidence (the calibrated classifier's probability on a category: label, 0.5 to 1); every stated condition must hold. Rules are ordered and the first match wins; the level is observe unless said otherwise. A matching rule hands the next matching item to the assignee, tells it through the delegation notice, and Today shows why. Requires assistant:write; an agent token is refused.
Response 201
| id | string | |
| name | string | |
| enabled | boolean | |
| position | integer | |
| match | AssistantRuleMatch | |
| assignee | object | |
| level | string | |
| grants | AssistantGrants | |
| match_count | integer | |
| last_matched_at | string (date-time) | null | |
| created_at | string (date-time) | |
| updated_at | string (date-time) |
/api/assistant/rules/{id}Change a standing delegation
Rename, enable or disable, reorder, change the match, the assignee, the level or the grants. Lowering the level to observe is the one-click kill switch of a rule. Requires assistant:write; an agent token is refused.
Parameters
| id* | string |
Response 200
| id | string | |
| name | string | |
| enabled | boolean | |
| position | integer | |
| match | AssistantRuleMatch | |
| assignee | object | |
| level | string | |
| grants | AssistantGrants | |
| match_count | integer | |
| last_matched_at | string (date-time) | null | |
| created_at | string (date-time) | |
| updated_at | string (date-time) |
/api/assistant/rules/{id}Delete a standing delegation
Removes the rule; the items it handed over come back to the owner, the receipts of what the assignee did stay. Requires assistant:write; an agent token is refused.
Parameters
| id* | string |
Response 200
| deleted | boolean | |
| released_items | integer |
/api/assistant/rules/testTry the standing delegations on sample facts
A dry run: which rule would take a matter with these facts (mailbox, key, sender, subject, labels, occurrences), and why. Nothing changes. Requires assistant:write; an agent token is refused.
Request body
| mailbox_id* | string | |
| key* | string | The work item key, for example org:tucows:invoice |
| sender_address | string | null | |
| sender_kind | string(human | automated) | |
| subject | string | null | |
| labels | string[] | |
| occurrences | integer | |
| confidence | object | The classifier's probability per label, for a rule with minConfidence |
Response 200
| matched | boolean | |
| rule | AssistantRule | |
| why | string[] | |
| considered | integer |
/api/assistant/rules/{id}/checkTry an effect against a standing delegation's level and grants
A dry run of the validation order for an effect requested under the rule today: the level allows the kind; the grants allow the recipients, the volume (today's executed effects count), the schedule, the spend, the risk classes read off the matter; the required approver approved. Answers the status a plan would take (proposed, approved, escalated or rejected) with every check and why. Nothing changes. Requires assistant:write; an agent token is refused.
Parameters
| id* | string |
Request body
| effect* | object | |
| matter | object | |
| at | string (date-time) | The instant to check the schedule at; now by default |
| approved_by | string[] | Member ids who approved already |
Response 200
| rule | object | |
| allowed_kinds | string[] | |
| effective_grants | AssistantGrants | |
| outcome | string | |
| check | AssistantGrantCheck | |
| hint | string |
/api/assistant/metricsRead the task metrics
The seven numbers of the task layer on a rolling window (thirty days by default, window_days from 1 to 90), for the account and for each standing delegation: matters closed without the owner, the time from arrival to done, the plans executed as proposed, edited and rejected, the escalations per matter, the corrections after execution, the collisions, the model cost per matter. Read from the receipts, the plans, the escalations and the ledger; nothing is estimated. Requires assistant:read.
Parameters
| window_days | integer |
Response 200
| type | string | |
| as_of | string (date-time) | |
| window_days | integer | |
| account | AssistantTaskNumbers | |
| rules | object[] |
/api/assistant/approvalsList the approvals queue
The plans waiting for the owner (proposed at draft, or held by a grant) and the questions the assignees asked, across the matters the reader may see. Answer through the work item's interactions: approve or reject a plan, choose an option or instruct on a question. Requires assistant:read. A matter made from authentication mail before such mail was kept out of the assistant is shown with its title, summary, ask and plan effects withheld from a reader without email:auth_secrets.
Response 200
| type | string | |
| as_of | string (date-time) | |
| plans | object[] | |
| escalations | object[] |
/api/assistant/suggestionsList the delegation suggestions
The standing delegations the assistant proposes: a matter that recurred at least three times in ninety days (the same organisation or sender) and was handled the same way every time (ignored, answered, closed, handed to one agent), with the rule prefilled. Open ones by default. Nothing is created until the owner accepts. Requires assistant:read; an agent token is refused.
Parameters
| status | string(open | accepted | dismissed) |
Response 200
| suggestions | AssistantSuggestion[] |
/api/assistant/suggestions/{id}Accept or dismiss a delegation suggestion
Accept: the suggestion closes and the prefilled rule comes back for the client to create (POST /api/assistant/rules). Dismiss: it closes and never comes back for the same matters and handling. Requires assistant:write; an agent token is refused.
Parameters
| id* | string |
Request body
| action* | string(accept | dismiss) |
Response 200
| suggestion | AssistantSuggestion | |
| rule | AssistantRuleInput |
/api/assistant/grantsList the grants attached to assignees
The grants attached to the owner's agents and members, on top of every rule that hands them matters; the stricter of the rule's and the assignee's applies. Requires assistant:read; an agent token is refused.
Response 200
| grants | AssistantAssigneeGrants[] |
/api/assistant/grantsSet the grants of an assignee
Attach grants to an agent identity or a workspace member; they replace the assignee's previous grants and cap every rule that hands them matters. Requires assistant:write; an agent token is refused.
Request body
| assignee_type* | string(agent | member) | |
| assignee_ref* | string | The agent identity slug, or the member's user id |
| grants* | AssistantGrants |
Response 200
| assignee | object | |
| grants | AssistantGrants | |
| created_at | string (date-time) | |
| updated_at | string (date-time) |
/api/assistant/grants/{type}/{ref}Remove the grants of an assignee
The assignee keeps only what each rule grants. Requires assistant:write; an agent token is refused.
Parameters
| type* | string(agent | member) | |
| ref* | string |
Response 200
| deleted | boolean |
/api/assistant/tasks/{id}Read a task envelope
The matter handed over, as the assignee needs it: the brief with the evidence, the facts, the delegation (assignee, rule, level, the effect kinds allowed, the effective grants), the plans open and recent, the lease. An agent token reads the matters handed to it and those of the mailboxes it operates; the owner reads their own. Requires assistant:read. A matter made from authentication mail before such mail was kept out of the assistant is shown with its title, summary, ask and plan effects withheld from a reader without email:auth_secrets.
Parameters
| id* | string | The work item id |
Response 200
| id | string | The work item id |
| version | integer | |
| status | string | |
| deadline | string (date-time) | null | |
| you | object | |
| delegation | object | |
| lease | AssistantTaskLease | |
| brief | object | null | The matter's brief, as GET /api/assistant/work-items/{id}/brief answers it |
| facts | object | null | The matter's facts, as GET /api/assistant/work-items/{id}/facts answers them |
| plans | object | |
| escalations | object[] | The questions asked on the task, newest first, with their answers |
/api/assistant/tasks/{id}/leaseTake or renew the lease on a task
Two assignees never work one matter: the lease names who works it until when (fifteen minutes by default, up to an hour). Taking it again renews it; another holder is refused with TASK_LEASED until the expiry. An effects request takes the lease on its own. Requires assistant:write.
Parameters
| id* | string |
Request body
| ttl_seconds | integer |
Response 200
| task_id | string | |
| lease | AssistantTaskLease |
/api/assistant/tasks/{id}/leaseGive the lease back
Releases the lease the caller holds; a lease held by another assignee is left alone. Requires assistant:write.
Parameters
| id* | string |
Response 200
| task_id | string | |
| released | boolean | |
| lease | AssistantTaskLease |
/api/assistant/tasks/{id}/effectsAsk for effects on a task
The assignee asks for one or more effects (reply, forward, label, snooze, resolve, note, request). The delegation's level decides the kinds allowed and the grants cap them: inside the grants at guarded_send or operate (and the inward kinds at triage) the effects run at once with their receipts; at draft they wait for the owner's approval; a failed grant escalates to the owner; a kind the level does not allow is refused. The owner's own request runs at once. The same idempotency key answers the first plan again. Requires assistant:write.
Parameters
| id* | string |
Request body
| effects* | AssistantEffect[] | |
| idempotency_key* | string | |
| note | string | What the assignee wants the owner to know about this request |
Response 200
| task_id | string | |
| replayed | boolean | |
| plan | AssistantPlan | |
| execution | object | null | |
| lease | AssistantTaskLease | |
| hint | string |
/api/assistant/tasks/{id}/escalateStop and ask the owner
The assignee asks a question with the evidence and, when useful, the options it sees. The owner answers with the ordinary choose or instruct interactions on the decision this creates; the matter shows in Today as needing the owner, the lease is released, the task waits. Requires assistant:write.
Parameters
| id* | string |
Request body
| question* | string | |
| options | object[] | |
| evidence_refs | string[] |
Response 200
| task_id | string | |
| escalation_id | string | |
| decision_id | string | |
| status | string | |
| version | integer | |
| hint | string |
/api/assistant/tasks/{id}/reportReport on a task
What the assignee did and how it ended: done (the matter resolves, or waits when an answer is still expected), blocked (the owner is asked), or handed back (the owner takes over and the rule stays off this matter). Claims are reconciled with the receipts the platform wrote: a claim without a receipt is recorded as a claim, never as a fact. The lease is released. Requires assistant:write.
Parameters
| id* | string |
Request body
| outcome* | string(done | blocked | handed_back) | |
| summary* | string | |
| evidence_refs | string[] | |
| claims | object[] |
Response 200
| task_id | string | |
| outcome | string | |
| status | string | |
| reconciliation | object | |
| receipt_id | string | |
| hint | string |
/api/emails/{address}/messages/{id}/factsRead what the assistant established about a message
The knowledge the analyses of this message produced: claims (subject, predicate, value with type and currency, basis, confidence, validity, evidence), entities, commitments and expectations. Nothing is computed on the way: a message the pipeline never analysed answers analysed: false with reason (not_enabled when the mailbox is not in the assistant, not_analysed when the analysis has not happened yet). Requires assistant:read.
Parameters
| address* | string | |
| id* | string | Message ID |
Response 200
| message_id | string | |
| thread_id | string | |
| analysed | boolean | |
| reason | string | Only when analysed is false |
| analysis | object | |
| facts | Fact[] | |
| entities | object[] | |
| commitments | object[] | |
| expectations | object[] |
/api/emails/{address}/messages/{id}/extractExtract the fields you need from a message with your own schema
One model call on the message text (subject, sender, body) with a caller JSON schema of objects and primitives (up to 8 KB, no references): answers data in that shape, confidence (0 to 1) and evidence_quotes (verbatim excerpts). Cached by (message, schema, instructions) so the same question costs one call (cached: true after). Gated by the account's model budget (429 when spent, 503 when the model route is off). Sign-in code mails answer 403 AUTH_MESSAGE. Bodies over 60,000 characters answer 413.
Parameters
| address* | string | |
| id* | string | Message ID |
Request body
| schema* | object | JSON schema of the fields to extract: an object of primitives, arrays and nested objects, under 8 KB, no $ref |
| instructions | string | Optional guidance, e.g. amounts in EUR |
Response 200
| message_id | string | |
| data | object | |
| confidence | number | |
| evidence_quotes | string[] | |
| model | string | |
| cached | boolean | |
| generated_at | string (date-time) | |
| usage | object |
/api/assistant/settingsGet Mailzero assistant settings
Returns the assistant opt-in state, shadow/pause flags, consent version, privacy route, mailbox scope, and every mailbox you own. Requires the assistant:read scope.
Response 200
| enabled | boolean | |
| shadow_enabled | boolean | |
| paused | boolean | |
| mailbox_ids | string[] | |
| agent_mailbox_ids | string[] | Mailboxes operated by your agents. Their items appear in the agents section of Today instead of Needs you. |
| agents | object[] | Your agent identities, for delegation. |
| search_opt_out_mailbox_ids | string[] | Mailboxes whose mail is searched by words only: their messages are never sent to the embedding model. |
| brief_opt_out_mailbox_ids | string[] | Mailboxes whose briefs stay deterministic: no evidence excerpt is sent to the model for the summary and next move (the analysis of arriving mail is unchanged) |
| history_window_days | integer | |
| privacy_route | string | |
| attachment_vision | boolean | |
| community_signals | boolean | Your phishing reports count toward the community fingerprints (a domain, the shape of a subject, never a text or an address) and the community's counts reach your trust signals. On by default; off keeps both local. |
| pipeline_version | string | |
| consent | object | |
| mailboxes | object[] |
Example response
{
"enabled": false,
"shadow_enabled": true,
"paused": false,
"mailbox_ids": [
"mbx_123"
],
"history_window_days": 30,
"privacy_route": "private_cloud",
"attachment_vision": false,
"pipeline_version": "communication-analysis.v1",
"consent": {
"required_version": "mailzero-personal-ai.v1",
"accepted_version": "mailzero-personal-ai.v1",
"accepted_at": "2026-09-02T09:12:00.000Z"
},
"mailboxes": [
{
"id": "mbx_123",
"slug": "hello",
"domain": "example.com",
"name": "Hello",
"kind": "api",
"address": "hello@example.com"
}
]
}/api/assistant/settingsUpdate Mailzero assistant settings
Explicitly enables, pauses, resumes, or scopes assistant processing. Sending mailbox_ids: [] disables every mailbox and cancels a running history import. Resuming requires the current consent version. Mail receipt remains independent. Requires the assistant:write scope.
Request body
| enabled | boolean | Show Today Decisions and generate drafts on demand |
| shadow_enabled | boolean | Analyze mail without surfacing Decisions |
| paused | boolean | |
| mailbox_ids | string[] | Owned mailbox IDs to process; an empty array disables everything |
| agent_mailbox_ids | string[] | Mailboxes operated by your agents. Their items appear in the agents section of Today instead of Needs you. |
| history_window_days | integer | |
| attachment_vision | boolean |
Response 200
| ok | boolean | |
| enabled | boolean | |
| shadow_enabled | boolean | |
| paused | boolean | |
| mailbox_ids | string[] |
Example response
{
"ok": true,
"enabled": true,
"shadow_enabled": true,
"paused": false,
"mailbox_ids": [
"mbx_123"
]
}/api/assistant/previewConsent to and start a background history import
Records the current explicit AI consent, enables shadow processing on the selected user-owned mailboxes, and creates a resumable background import of recent history. The client may close immediately and poll GET /api/assistant/backfill. Requires the assistant:write scope.
Request body
| mailbox_ids* | string[] | |
| history_window_days | integer | |
| consent* | boolean(true) | |
| consent_version* | string(mailzero-personal-ai.v1) |
Response 200
| ok | boolean | |
| preview | object |
Example response
{
"ok": true,
"preview": {
"mailbox_ids": [
"mbx_123"
],
"history_window_days": 30,
"scanned": 0,
"enqueued": 0,
"capped": false,
"background": true,
"backfill_id": "bf_456"
}
}/api/assistant/backfillGet background history-import progress
Returns scanning/analysis phase, processed and total messages, percentage, adaptive ETA, pause state, and whether the app may close. Requires the assistant:read scope.
Response 200
| backfill | object | null |
Example response
{
"backfill": {
"id": "bf_456",
"status": "processing",
"phase": "analysis",
"paused": false,
"processed": 12,
"total": 40,
"failed": 0,
"percent": 30,
"estimated_seconds_remaining": 240,
"estimated_completion_at": "2026-09-02T09:20:00.000Z",
"can_close_app": true,
"started_at": "2026-09-02T09:12:00.000Z",
"completed_at": null
}
}/api/assistant/backfillRetry failed history-import messages
Requeues terminal failures from the latest import for mailboxes that remain AI-enabled. Automatic retries have already been exhausted before a message appears here. Requires the assistant:write scope.
Response 200
| ok | boolean | |
| retried | integer |
Example response
{
"ok": true,
"retried": 2
}/api/assistant/todayGet the grounded Today decision queue
Returns the versioned Now, Needs you, Waiting, Upcoming, and recent Handled projections with as_of, lag, coverage, and warnings. Every item links to its source message; nothing here sends mail. Requires the assistant:read scope. Authentication mail (sign-in codes, password resets, magic links) never becomes a matter and never reaches a model. A matter made from authentication mail before such mail was kept out of the assistant is shown with its title, summary, ask and plan effects withheld from a reader without email:auth_secrets.
Parameters
| agent | string | Only one agent's view: the items of the mailboxes it operates and the items handed to it. A token bound to an agent identity always gets that agent's view; naming another agent answers 403 AGENT_SCOPE. |
| category | string | Only one kind of matter (invoice, meeting_request, newsletter...); the response's categories field lists every kind in the view with its count. An unknown kind answers 400 INVALID_CATEGORY. |
| tag | string | Only the matters carrying this tag (one of the person's tags); the response's tags field lists every tag in the view with its count. |
Response 200
| type | string | |
| as_of | string | |
| projection_lag_ms | integer | |
| category | string | null | The kind of matter the view is narrowed to (the category query), or null for every kind. |
| categories | object | Every kind of matter in the view, before the filter, with how many items it holds. |
| tag | string | null | The tag the view is narrowed to (the tag query), or null for every matter. |
| tags | object | Every tag of the person in the view, before the filter, with how many matters carry it. |
| sections | object |
Example response
{
"type": "today",
"as_of": "2026-09-02T09:15:00.000Z",
"projection_lag_ms": 4200,
"category": null,
"categories": {
"partner_inquiry": 1
},
"tag": null,
"tags": {
"lumenworks": 1
},
"sections": {
"now": [
{
"id": "wi_789",
"version": 3,
"status": "needs_judgment",
"title": "Investor update",
"summary": "Paul needs the August metrics before his 16:00 partner call.",
"category": "partner_inquiry",
"tags": [
"lumenworks"
],
"proposed_tags": [],
"mailbox": {
"id": "mbx_123",
"address": "hello@example.com",
"name": "Hello"
},
"source": {
"type": "domani-postgres",
"conversation_id": "conv_1",
"event_id": "msg_1",
"revision": 1
},
"attention": {
"level": "now",
"reason": "Deadline today from a known investor",
"confidence": "high",
"deadline": "2026-09-02T16:00:00.000Z"
},
"decision": {
"id": "dec_1",
"version": 1,
"status": "open",
"question": "How do you want to answer Paul?",
"options": [
{
"id": "opt_send",
"label": "Send current metrics",
"recommended": true
},
{
"id": "opt_tomorrow",
"label": "Ask for tomorrow"
}
]
}
}
],
"needs_you": [],
"waiting": [],
"upcoming": [],
"handled": []
}
}/api/assistant/work-items/{id}Get one assistant Work item and exact current plan
Returns the grounded Work item, its open Decision and options, the exact current Action plan, open commitments and expectations, interactions, and receipts. Requires the assistant:read scope. A matter made from authentication mail before such mail was kept out of the assistant is shown with its title, summary, ask and plan effects withheld from a reader without email:auth_secrets.
Parameters
| id* | string | Work item ID from the Today queue |
Response 200
| id | string | |
| version | integer | |
| status | string | |
| title | string | |
| summary | string | |
| ask | string | null | The single thing the owner must do or decide, or null. |
| category | string | null | The kind of matter the analysis found (an invoice, a meeting request, a newsletter...), or null when no analysis said. Rules match it as the label category:<kind>. |
| tags | string[] | The person's tags on the matter (CT-01b): confirmed once or set by hand, each one a label on the thread at the provider. Rules match them as the label tag:<name>. |
| proposed_tags | string[] | The tags the engine proposes for the matter (one per project it named), not yet confirmed. Confirm one with the tag interaction; it then applies to the next matter of the same project without a question. |
| suspect | boolean | The trust signals found the sender suspect: the matter sits in the Suspect section, is never Now, carries no decision, and no effect runs on it until the person reports it legitimate. |
| suspect_reasons | string[] | Why it looks suspect, one sentence per signal (a failed authentication, a diverging Reply-To, a lookalike domain, a borrowed name, a first contact about money, an instruction to the assistant, a secret asked by return mail). |
| sender | object | null | |
| recurrence | object | How often this same situation came back: one item, N messages. |
| mailbox | object | |
| opened_at | string | null | |
| delegated_to | string | null | The slug of the agent the owner handed this item to, or null. |
| delegation_rule | object | null | The standing delegation that handed this item over, and why it matched; suspended once the owner took the item back. |
| unread | boolean | True until the owner opens the item after its latest change. |
| messages | object[] | The source messages behind this item, newest first. |
| history | object[] | What already happened in this matter, oldest first: one line per earlier message. |
| source | object | |
| attention | object | |
| analysis | object | |
| evidence_refs | string[] | |
| entity_refs | string[] | |
| snoozed_until | string | null | |
| decision | object | null | |
| plan | object | null |
/api/assistant/work-items/{id}/briefRead the situation brief of a Work item
One document an agent reads before acting: the situation (status, attention, recurrence, sender), the story so far (every mail of it, analysed or not), the facts the analyses established (claims with value, basis and validity, and the entities), the open commitments and expectations with what is awaited from whom and since when, what already settled (closed loops and receipts), the current Decision with its options, a suggested next step (the Decision's recommended option when there is one, otherwise a rule: an overdue promise, an overdue expectation to chase, the sender's ask to answer), and the evidence messages. Deterministic and cheap: no model call. Agent tokens read the items of the mailboxes they operate and the items handed to them.
Parameters
| id* | string | Work item ID |
| synthesis | boolean | Set to false to skip the model's reading (summary, concrete next action, open questions) and read the deterministic brief only; the reading is cached per state of the evidence |
Response 200
| id | string | |
| version | integer | |
| generated_at | string (date-time) | |
| title | string | |
| summary | string | |
| ask | string | null | |
| mailbox | object | |
| delegated_to | string | null | |
| situation | object | |
| story | object[] | Every mail of the situation, oldest first, with the one-line title the analysis gave it |
| facts | object[] | |
| entities | object[] | |
| commitments | object[] | Open promises made by the owner's side |
| expectations | object[] | Open things awaited from others |
| settled | object[] | Closed loops and receipts, newest first |
| awaiting | object[] | |
| decision | object | null | The current prepared Decision with its options |
| summary_source | string | Whether the summary is the model's reading of the evidence or the pipeline's own sentence |
| suggested_next | object | null | |
| open_questions | string[] | What only the owner can decide before the next action, at most three questions |
| synthesis | object | null | The model call that wrote the summary and next action, null when the brief is deterministic only |
| evidence | object[] | |
| owner_replies | object[] | |
| evidence_refs | string[] |
/api/assistant/briefRead the situation brief for a correspondent
What to know before answering a person: the brief of their most recent active Work item across the mailboxes you may read (or one mailbox), the other items they are behind, and their last ten mails. brief is null when nothing was analysed from them yet.
Parameters
| correspondent* | string | The person's email address |
| mailbox | string | Restrict to one mailbox address (slug@domain) |
Response 200
| correspondent | string | |
| mailboxes | string[] | |
| brief | any | |
| related | object[] | |
| recent_mail | object[] | |
| generated_at | string (date-time) |
/api/assistant/work-items/{id}/factsRead the facts behind a Work item
The claims and entities merged from every analysis of the item (newest first wins), plus its stored commitments and expectations with their status. Agent tokens read the items of the mailboxes they operate and the items handed to them.
Parameters
| id* | string | Work item ID |
Response 200
| work_item_id | string | |
| version | integer | |
| analysed | boolean | |
| analyses | object[] | |
| facts | Fact[] | |
| entities | object[] | |
| commitments | object[] | |
| expectations | object[] | |
| evidence_refs | string[] |
/api/assistant/work-items/{id}/openMark a Work item as opened
Records that the owner opened this Work item at its current version, which clears its unread marker until the item changes again. Requires the assistant:write scope.
Parameters
| id* | string |
Response 200
| ok | boolean | |
| opened_at | string | |
| unread | boolean |
/api/assistant/learnedWhat the assistant learned about you
The quiet rules the assistant proposes after the same sender or organisation was ignored every time it came back, the mutes in force (a sender, a domain or a situation the assistant keeps out of Today), each with why it was set, and the tags confirmed once, each applied to the next matter of the same project without a question. Accept a quiet rule through the suggestions endpoint; forget a mute with the mutes endpoint, a tag with the tags endpoint.
Response 200
| mutes | object[] | The mutes in force, newest first. |
| suggestions | AssistantSuggestion[] | The open quiet rules: accept one through the suggestions endpoint to set the mute it carries. |
| tags | object[] | The tags confirmed once (CT-01b), newest first: each applies to the next matter of the same project without a question until forgotten. |
/api/assistant/learned/mutesForget a mute
The next mail of that sender, domain or situation becomes an item again.
Request body
| kind* | string(sender | sender_domain | situation) | |
| value* | string | The address, the domain or the situation key, as the learned view lists it. |
Response 200
| ok | boolean | |
| kind | string | |
| value | string |
/api/assistant/learned/tagsForget a tag
Nothing is applied without a question any more: the next matter of that project proposes the tag again. The matters that carry it keep it.
Request body
| name* | string | The tag, as the learned view lists it. |
Response 200
| ok | boolean | |
| name | string |
/api/assistant/work-items/{id}/investigateInvestigate a matter: what the registry and the links say
Reads, at that instant, the facts public infrastructure holds about the sender's domain (its registration date and registrar, its SPF and DMARC posture, whether it receives mail), where every link of the source message really goes (the redirects followed one hop at a time, the final host and its domain's facts), and what each attachment really is (its first bytes against the name and the declared type). Answers the dossier with the signals the facts add (a domain registered days ago, a link that lands elsewhere, a sender's domain that protects nothing, a program named as a document). No vendor, no account, nothing stored; a lookup that fails is a fact of its own.
Parameters
| id* | string |
Response 200
| work_item_id | string | |
| source | string | Whether the source message was read, or only the sender's address. |
| checked_at | string | |
| sender | object | |
| links | object[] | |
| attachments | object[] | What each attachment really is, read from its first bytes against what its name and declared type promise. |
| signals | object[] | What the facts add to the assessment, one sentence each. |
/api/assistant/work-items/{id}/senderWho is behind a matter: the organisation, the people involved, the history
Answers the sender card of a matter: the sender's organisation as the stored domain profile knows it (name, legal name, category, website, registration date, mail posture; null for a shared mail domain such as gmail.com or a domain never analysed, and never a live analysis), the people the story's messages carry in from, to and cc with their roles and whether they belong to the same organisation (the reader's own mailboxes left out), and the relationship: how much mail came from this address across the reader's mailboxes (counted up to 50), when first and last, the sender's other matters and the matters raised by colleagues at the same domain. Requires the assistant:read scope.
Parameters
| id* | string |
Response 200
| work_item_id | string | |
| sender | object | null | |
| organisation | object | null | Null for a shared mail domain or a domain never analysed. |
| participants | object[] | |
| relationship | object | |
| generated_at | string (date-time) |
/api/assistant/work-items/{id}/interactionsRecord a fenced human Decision
Choose an option, give an instruction, take over, snooze, ignore, correct, report the trust verdict (phishing or legitimate), or approve or reject a waiting plan (an approved plan executes at once, with its receipts in the answer). The call is fenced by work_item_version and replayed idempotently by idempotency_key. Choosing or instructing prepares an exact draft that still goes through the normal reply flow; this endpoint never sends mail. Requires the assistant:write scope.
Parameters
| id* | string |
Request body
| scope | string(item | sender | sender_domain | situation) | With type ignore: also mute the sender, its domain, or this recurring situation so it never comes back. |
| verdict | string(phishing | legitimate) | With type report: the person's verdict on the trust signals. Phishing closes the matter, mutes the sender and closes its other live matters; legitimate clears the suspect verdict and its reasons. |
| plan_id | string | With type approve or reject: the waiting action plan (proposed or escalated). |
| reason | string | With type reject: why, kept on the plan. |
| type* | string(choose | instruct | take_over | snooze | ignore | correct | delegate | approve | reject | report | tag) | |
| add | string[] | With type tag: tags to put on the matter (a proposed one confirmed, or one by hand; lower case, letters, digits and hyphens). A tag added once is learned: the next matter of the same project gets it without a question. |
| remove | string[] | With type tag: tags to take off the matter (a proposal named here is declined). The learned page forgets a tag for good. |
| agent_slug | string | null | With type delegate: the slug of your agent to hand the item to, or null to take it back. |
| member_id | string | null | With type delegate: the user id of a member of your workspace to hand the item to instead of an agent; they find it under handed_to_you in their Today and work it through the tasks endpoints under their own access. |
| work_item_version* | integer | |
| decision_id | string | |
| decision_version | integer | |
| option_id | string | |
| instruction | string | |
| until | string (date-time) | |
| remind_at | string (date-time) | |
| field | string | |
| correction | string | |
| idempotency_key* | string |
Response 200
| ok | boolean | |
| work_item | AssistantWorkItem | |
| plan_id | string | null |
/api/assistant/action-plans/{id}Get the exact action preview by opaque ID
Returns the exact reply identity, recipients, body, commitments, and side effects of a prepared Action plan, plus its expiry and source revision, so a client can show what will happen before the user sends through the existing reply endpoint with assistant_plan_id. Requires the assistant:read scope.
Parameters
| id* | string | Opaque Action plan ID |
Response 200
| id | string | |
| status | string | |
| expires_at | string | |
| source_revision | integer | |
| effects | object[] |
/api/assistant/activityGet content-minimized assistant Activity
Lists recent interactions and receipts attributed to the caller without message bodies. Requires the assistant:read scope.
Parameters
| limit | integer |
Response 200
| type | string | |
| as_of | string | |
| events | object[] |
/api/assistant/dataExport derived assistant data
Exports analyses, accepted claims, Decisions, interactions, receipts, and model usage as a JSON attachment. It never includes source email bodies or attachment bytes. Requires the assistant:write scope. It also needs email:auth_secrets (403 INSUFFICIENT_SCOPE otherwise): the derived state can hold what the assistant drew from authentication mail before such mail was kept out of it.
Response 200
| format | string | |
| exported_at | string | |
| note | string |
/api/assistant/dataDelete assistant-derived data
Idempotently deletes derived intelligence, jobs, model usage, devices, imports, and settings while preserving source mail. Requires the assistant:write scope.
Response 200
| ok | boolean | |
| deleted | object | |
| source_mail_deleted | boolean |
Example response
{
"ok": true,
"deleted": {
"work_items": 12,
"claims": 40,
"entities": 9,
"jobs": 52
},
"source_mail_deleted": false
}/api/assistant/devicesRegister a device for redacted Now alerts
Registers or rotates an encrypted push token for one app installation and environment. Only the native app calls this; it requires an active assistant and the assistant:write scope.
Request body
| installation_id* | string | |
| platform* | string(ios | android) | |
| environment* | string(development | preview | production) | |
| push_token* | string | |
| locale* | string | |
| lock_screen_detail | string(redacted | sender_only) |
Response 200
| ok | boolean | |
| device | object |
/api/assistant/devicesRevoke a device registration
Revokes every push registration of one installation, for example on sign-out. Requires the assistant:write scope.
Parameters
| installation_id* | string |
Response 200
| ok | boolean |
/api/domains/{domain}/email/checkCheck email DNS health (MX, SPF, DMARC, DKIM)
Checks email DNS configuration for a domain: MX records, SPF, DMARC, and DKIM. Auto-detects the email provider from MX records (Google Workspace, Proton, Fastmail).
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| provider | string | null | Auto-detected email provider (google-workspace, proton, fastmail) or null |
| mx | object | |
| spf | object | |
| dmarc | object | |
| dkim | object |
Example response
{
"domain": "mysite.com",
"provider": "google-workspace",
"mx": {
"configured": true,
"records": [
"1 aspmx.l.google.com",
"5 alt1.aspmx.l.google.com"
]
},
"spf": {
"configured": true,
"value": "v=spf1 include:_spf.google.com ~all"
},
"dmarc": {
"configured": true,
"value": "v=DMARC1; p=quarantine; rua=mailto:dmarc@mysite.com"
},
"dkim": {
"configured": true,
"selectors": [
"google"
]
}
}Example
curl https://domani.run/api/domains/mysite.com/email/check \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/email/deliverabilityGet owner-scoped email deliverability health
Separates deterministic authentication readiness, 30-day recipient-weighted delivery outcomes, account safety state, and measured provider placement. The readiness score is not an Inbox probability, and unmeasured placement is returned as not_tested.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| status | string | |
| confidence | string | |
| checked_at | string (date-time) | |
| window_days | integer | |
| readiness | object | Deterministic DNS configuration readiness. Score is not an Inbox probability. |
| sending_health | object | |
| placement | object | |
| actions | object[] | |
| caveats | string[] |
Example
curl https://domani.run/api/domains/mysite.com/email/deliverability \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/email/setupSet up email on a domain
Pre-configure email DNS on a domain (optional). This is called automatically by create_mailbox when needed. Pass force=true to override existing email provider (Google Workspace, Fastmail, Proton).
Parameters
| domain* | string | Domain name |
Request body
| force | boolean | Override existing email provider (Google Workspace, Fastmail, Proton). Default: false |
Response 200
| domain | string | |
| status | string | |
| verified_at | string | null | |
| receiving_verified | boolean | True when the domain's MX points at the platform's receiver, so mail for its addresses is accepted. |
| sending_configured | boolean | |
| sending_verified | boolean | |
| records | DnsRecord[] | |
| hint | string |
/api/domains/{domain}/email/setupRemove email from a domain
Remove email from a domain, delete all mailboxes and messages, and set emailEnabled to false. It changes mail for the whole domain, so a mailbox-confined token gets 403 MAILBOX_CONFINED.
Parameters
| domain* | string | Domain name |
/api/domains/{domain}/email/statusCheck email DNS status
Check email DNS verification status for a domain. Returns whether the domain is verified and ready, DNS record details, and mailbox count.
Parameters
| domain* | string | Domain name |
Response 200
| domain | string | |
| enabled | boolean | |
| verified | boolean | True only when both receiving and outbound sending are verified |
| receiving_verified | boolean | True when the domain's MX points at the platform's receiver, so mail for its addresses is accepted. |
| sending_configured | boolean | |
| sending_verified | boolean | |
| records | DnsRecord[] | |
| mailbox_count | integer | |
| dns_managed | boolean | True when Domani writes the domain's zone itself: pending records are published and repaired by an hourly audit, nothing to add by hand. False for an external zone, where pending records must be added at the DNS provider. |
| unreachable_addresses | string[] | Addresses on the domain that its receiving MX refuses (api mailboxes on a hosted domain not yet registered on the mail host). Empty when every address can receive; registration is retried automatically. |
| provider_status | string | |
| mailbox_plan_key | string | The workspace's plan key that governs custom-domain mailboxes here (the workspace's own subscription, not necessarily the signed-in user's personal plan). |
| mailbox_limit | integer | null | Custom-domain mailboxes the plan allows across the workspace. null is unlimited. |
| mailbox_limit_reached | boolean | True once the workspace is at its plan's mailbox limit: another mailbox on this domain needs an upgrade first. |
| mailbox_upgrade_plan | object | null | The cheapest plan that would give room for another mailbox. null when already unlimited. |
| hint | string |
/api/emailCreate a mailbox on domani.run
Create a free email address on domani.run (e.g. hello@domani.run). No domain ownership required. Limited to 1 free @domani.run mailbox per user. Many common slugs are reserved.
Request body
| slug* | string | Local part of the email address (e.g. 'hello' for hello@domani.run) |
| name | string | Display name (defaults to capitalized slug) |
Response 201
| id | string | |
| address | string | |
| name | string | null | |
| slug | string | |
| domain | string | |
| kind | string | |
| workspace_id | string | null | |
| access_role | string | |
| effective_capabilities | MailboxEffectiveCapabilities | |
| collaboration_enabled | boolean | |
| shared | boolean | Whether at least one principal other than the caller can read this mailbox |
| message_count | integer | |
| webhook_url | string | null | |
| header_names | string[] | Configured webhook authentication header names; values are never returned |
| forward_to | string | null | |
| signing_secret | string | null | |
| created_at | string (date-time) | |
| hint | string |
/api/emailList all mailboxes
List every mailbox the acting principal can access across all domains, including workspace grants.
Parameters
| domain | string | Filter by domain |
Response 200
| mailboxes | object[] |
/api/emailsCreate a mailbox
Create an email address (mailbox). Provide a full address like "hello@mysite.com". Email DNS is auto-configured on first use. Pass force=true to override an existing email provider. Custom-domain mailbox capacity is determined by the active account or workspace plan. Every account can create one free @domani.run mailbox separately with POST /api/email. Pass workspace_id to create the mailbox inside a workspace you own. For a custom domain, this explicitly and atomically adopts the domain and all its unscoped sibling mailboxes into that workspace; a domain can never be split across workspaces. Pass kind="hosted" to create a real IMAP/SMTP mailbox on your own domain (connect Apple Mail, Thunderbird, or any client) instead of the default API mailbox (send/receive via API + webhooks). Hosted mailboxes return DNS records to publish, mail client settings, and a one-time app password. Requires a domain you own.
Request body
| address* | string | Full email address, e.g. hello@mysite.com |
| name | string | Display name for outbound emails (e.g. "John Doe") |
| force | boolean | Override existing MX records. Default: false |
| kind | string(api | hosted) | "api" (default: send/receive via API + webhooks) or "hosted" (real IMAP/SMTP mailbox on your domain) |
| workspace_id | string | Workspace to own the mailbox. Only the workspace owner can adopt resources. |
Response 201
| id | string | |
| address | string | |
| name | string | null | |
| domain | string | |
| slug | string | |
| kind | string | |
| workspace_id | string | null | |
| access_role | string | |
| effective_capabilities | MailboxEffectiveCapabilities | |
| collaboration_enabled | boolean | |
| shared | boolean | Whether at least one principal other than the caller can read this mailbox |
| message_count | integer | |
| webhook_url | string | null | |
| header_names | string[] | Configured webhook authentication header names; values are never returned |
| forward_to | string | null | |
| signing_secret | string | null | HMAC secret for verifying inbound webhook payloads (shown once at creation) |
| host_id | string | Mail-host principal id for a hosted mailbox |
| dns_records | DnsRecord[] | |
| dns_status | string | |
| receiving_status | string | |
| sending_status | string | |
| sending_error | object | |
| client_settings | object | |
| credential | object | |
| storage_quota_bytes | integer | |
| created_at | string (date-time) | |
| hint | string |
/api/emailsList all mailboxes
List every mailbox the acting principal can access, including workspace grants. Same as GET /api/email.
Parameters
| domain | string | Filter by domain |
Response 200
| mailboxes | object[] |
/api/emails/{address}Get mailbox details
Get details for a specific mailbox by its full email address (URL-encoded, e.g. hello%40mysite.com).
Parameters
| address* | string | Full email address (URL-encoded, e.g. hello%40mysite.com) |
Response 200
| id | string | |
| address | string | |
| slug | string | |
| domain | string | |
| kind | string | |
| workspace_id | string | null | |
| access_role | string | |
| effective_capabilities | MailboxEffectiveCapabilities | |
| webhook_url | string | null | |
| forward_to | string | null | |
| signing_secret | string | null | HMAC secret for managers; null for viewers/responders |
| message_count | integer | |
| storage_quota_bytes | integer | null | |
| storage_used_bytes | integer | null | |
| storage_measured_at | string (date-time) | null | |
| created_at | string (date-time) |
/api/emails/{address}Update mailbox
Update a mailbox's display name, webhook URL, or forward-to email address by its full email address. Set to null to disable/clear.
Parameters
| address* | string | Full email address (URL-encoded) |
Request body
| name | string | null | Display name for the mailbox. Set to null to clear. |
| webhook_url | string | null | HTTPS URL for inbound email webhook, or null to clear |
| forward_to | string | null | Email address to forward inbound emails to, or null to clear |
| drop_spam | boolean | Drop (don't store or webhook) inbound messages flagged as spam |
| tone_notes | string | null | Voice hints agents read before drafting from this mailbox (tone, signature, phrasing dos and don'ts). Set to null to clear. |
Response 200
| id | string | |
| address | string | |
| slug | string | |
| domain | string | |
| name | string | null | |
| webhook_url | string | null | |
| forward_to | string | null | |
| tone_notes | string | null | |
| signing_secret | string | HMAC secret for verifying inbound webhook payloads |
/api/emails/{address}Delete a mailbox
Delete a mailbox by its full email address. Requires confirm=true in body. Without confirm, returns a preview with message_count.
Parameters
| address* | string | Full email address (URL-encoded) |
Request body
| confirm | boolean | Must be true to actually delete. Omit to preview. |
/api/emails/{address}/sendSend an email
Send an email from a mailbox identified by its full email address. The server runs a mandatory versioned policy first: objective safety failures are denied while reputation and content-quality findings are returned as warnings. For a collaborative threaded reply, pass the compose lease, observed reply_version, and a stable idempotency key; the server atomically rejects stale or double replies immediately before sending. Maximum 20 unique recipients per message. Rate limit: 100 recipients per hour per mailbox.
Parameters
| address* | string | Sender email address (URL-encoded) |
Request body
| to* | any | Recipient email address(es) |
| cc | any | CC recipient(s) |
| bcc | any | BCC recipient(s) |
| subject | string | |
| text | string | Plain text body |
| html | string | HTML body |
| reply_to | string | Reply-To address |
| in_reply_to | string | Message-ID of the email being replied to (for threading) |
| references | string | Space-separated Message-ID chain (for threading) |
| idempotency_key | string | Stable key that prevents duplicate sends |
| send_at | string (date-time) | Schedule the send (ISO 8601, 30 seconds to 30 days out). The message is stored with status 'scheduled' and dispatched server-side; move it to trash before then to cancel. Attachments are not supported with send_at yet. |
| coordination | object | Required for a guarded collaborative reply. |
| allow_risky_content | boolean | Deprecated compatibility field. Public HTTPS third-party auth links are allowed with a warning; objective safety blocks cannot be overridden. |
| attachments | object[] | File attachments (max 10, max 40MB total). Content must be base64-encoded. |
Response 200
| id | string | |
| message_id | string | RFC 5322 Message-ID |
| from | string | |
| to | string | |
| bcc | string | null | Bcc recipient(s) you sent to, echoed back on your own outbound message only; never delivered as a header and never present on a message you received. |
| status | string | |
| attachments_count | integer | |
| suppressed | string[] | |
| deliverability | object | Versioned policy report applied to this send; inspect decision and categorized checks |
/api/emails/{address}/deliverability-checkPreflight an outbound email
Run the exact versioned server-side policy used by the send endpoint without sending or consuming email quota. Public HTTPS authentication links on third-party domains are allowed with a reputation warning. Local/private URLs, non-HTTPS authentication links and executable attachments are blocked; bodies above 102 KB produce a clipping warning.
Parameters
| address* | string | Sender email address (URL-encoded) |
Request body
| subject | string | |
| text | string | |
| html | string | |
| idempotency_key | string | |
| allow_risky_content | boolean | Deprecated compatibility field; objective safety blocks cannot be overridden. |
| attachments | object[] |
Response 200
| policy_version | string | |
| decision | string | |
| safe_to_send | boolean | Backwards-compatible boolean equivalent to decision != deny |
| risk | string | |
| score | integer | |
| authentication_message | boolean | |
| checks | object[] | |
| blocked_reasons | string[] | |
| warnings | string[] | |
| override_used | boolean |
/api/emails/{address}/messagesList emails
List emails in a canonical folder or virtual view. Defaults to Inbox. Folder location is independent from message direction and spam verdict. Authentication mail (OTPs / verification codes, password resets, magic links) is tagged security: "auth". Tokens without the email:auth_secrets scope see those messages with subject and body REDACTED (redacted: true) - metadata (from, date) stays visible. The subject, q and attachments filters match what the caller may read: without the scope such a message matches only on its redacted subject and body, so a query for a code it holds finds nothing.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| cursor | string | Pagination cursor from previous response |
| limit | integer | Max messages to return |
| direction | string(in | out | all) | Filter by direction (default: all) |
| folder | string(inbox | archive | sent | spam | trash) | System folder. Mutually exclusive with view. Defaults to inbox. |
| label | string | Comma-separated labels: only messages carrying any of them |
| view | string(starred | all | needs_human | scheduled) | Virtual view. Mutually exclusive with folder. |
| since | string | ISO timestamp - return only messages created after this date |
| until | string | ISO timestamp - return only messages created before this date |
| from | string | Filter by sender address |
| to | string | Filter by recipient address |
| subject | string | Filter by subject |
| q | string | Broad case-insensitive search across sender, recipients, subject, and plain-text body |
| unread | boolean | Return unread messages only |
| attachments | boolean | Return messages that contain attachments only |
| spam | boolean | Filter by spam flag (pass false to hide spam-flagged inbound) |
Response 200
| messages | object[] | |
| next_cursor | string | null |
/api/emails/searchSearch mail by meaning and by words
Searches the mailboxes you may read, or one mailbox with mailbox. Hybrid by default: the message embedding (pgvector, cosine) and the weighted words index are fused by reciprocal rank with a small recency boost, in one statement; why tells which branch matched. Gmail operators in q: from: to: cc: subject: label: filename: mailbox: is:unread|read|starred|unstarred has:attachment in:inbox|archive|sent|spam|trash|anywhere after:2026/08/01 before: newer_than:7d older_than:2w (h/d/w/m/y), "a phrase", -word or -from: to exclude, a OR b. A query made of operators alone lists the matching messages newest first (why: filter). Sign-in codes and links, spam and system mail are never indexed; mailboxes listed in the assistant's search_opt_out_mailbox_ids are searched by words only. A token bound to an agent identity searches only the mailboxes its agent operates. Requires email:read. Without email:auth_secrets, inbound authentication mail is left out before ranking: no word, operator or filter matches it, so a search for a sign-in code finds nothing.
Parameters
| mailbox | string | Restrict to one mailbox address (slug@domain). |
| q* | string | What you are looking for, in any language: a name, a reference, or a description of the mail, plus Gmail operators (`from:dave is:unread newer_than:7d -invoice "exact phrase"`). |
| mode | string(hybrid | semantic | keyword) | hybrid fuses the embedding and the words by rank; semantic uses the meaning only; keyword the words only. |
| limit | integer | Results to return. |
| since | string | Only mail received after this ISO timestamp. |
| until | string | Only mail received before this ISO timestamp. |
| folder | string(inbox | archive | sent | spam | trash | anywhere) | One folder. Trash is excluded unless named or `anywhere`. |
| from | string | Sender address contains. |
| label | string | Comma-separated labels: only messages carrying any of them |
Response 200
| results | object[] | |
| mode | string | |
| query | string | |
| took_ms | integer | |
| hint | string |
Example
curl "https://domani.run/api/emails/search?q=facture+tucows+ao%C3%BBt&limit=5" \ -H "Authorization: Bearer domani_sk_xxx"
/api/emails/connect/gmailStart connecting a Gmail inbox
Returns the Google consent URL bound to your account and to the import bound. Open it in a browser; after consent the inbox becomes a connected mailbox at its own address, the mail received after the bound is imported (default: the last 24 hours), and new mail follows as it arrives. Never the whole inbox: widen later with POST /api/emails/{address}/import. Requires email:write. Google OAuth must be configured on the server.
Request body
| access | string(read | write) | read (the default) reads the inbox; write also lets the platform change labels and the read state and send as the person, for the effects of a standing delegation. Reconnect with write to widen a read-only grant. |
| since | string (date-time) | Import the mail received after this instant |
| window | string(24h | 7d | 30d | 1y) | A preset instead of since; default 24h |
| workspace_id | string | Workspace the mailbox belongs to; your personal workspace by default |
| login_hint | string | The Google account to preselect |
| return_to | string | App path to send the browser to after consent |
Response 200
| auth_url | string | |
| state | string | |
| import_since | string (date-time) | |
| expires_in | integer | |
| access | string |
/api/emails/connect/outlookStart connecting an Outlook inbox
Returns the Microsoft consent URL bound to your account and to the import bound. Open it in a browser; after consent the inbox becomes a connected mailbox at its own address, the mail received after the bound is imported (default: the last 24 hours), and new mail follows as it arrives. Never the whole inbox: widen later with POST /api/emails/{address}/import. Requires email:write. Microsoft OAuth must be configured on the server.
Request body
| access | string(read | write) | read (the default) reads the inbox; write also lets the platform change labels and the read state and send as the person, for the effects of a standing delegation. Reconnect with write to widen a read-only grant. |
| since | string (date-time) | Import the mail received after this instant |
| window | string(24h | 7d | 30d | 1y) | A preset instead of since; default 24h |
| workspace_id | string | Workspace the mailbox belongs to; your personal workspace by default |
| login_hint | string | The Microsoft account to preselect |
| return_to | string | App path to send the browser to after consent |
Response 200
| auth_url | string | |
| state | string | |
| import_since | string (date-time) | |
| expires_in | integer | |
| access | string |
/api/emails/connect/providersList the ways an inbox can be connected
One row per provider (Gmail, Outlook, any inbox by forwarding, an address hosted here): how the connection starts (a consent screen, a forwarding rule, an address created), what the connection can do (read, or read and write), whether it imports older mail, and whether this server is set up for it. Show a connect button only when configured is true; reason names the missing variable for the operator. Requires email:read.
Response 200
| providers | MailProvider[] |
/api/emails/connect/forwardingConnect an inbox by forwarding
Creates a connected mailbox at the inbox's own address and answers with a forwarding address on the platform's forwarding domain, with the steps for Gmail, Outlook and other providers. Add a forwarding rule at the provider; what arrives at that address is unwrapped (original sender, date, subject) and stored under the connected mailbox. Gmail asks the forwarding address to confirm: the code and the link appear on the connection. The first mail that comes back, or a probe (POST /api/emails/{address}/connector/verify), makes the connection active. Only new mail arrives this way; older mail cannot be imported. Requires email:write. The forwarding domain must be configured on the server.
Request body
| address* | string | The inbox to connect, for example someone@gmail.com |
| workspace_id | string | Workspace the mailbox belongs to; your personal workspace by default |
Response 200
| address | string | |
| mailbox_id | string | |
| status | string | |
| forwarding_address | string | |
| instructions | object | |
| guide | ForwardingGuide |
/api/emails/{address}/connector/verifySend a probe through the forwarding rule
Sends a mail to the connected inbox; when the forwarding rule brings it back to the forwarding address, the connection becomes active. Only for a connection by forwarding. Requires email:write.
Parameters
| address* | string | The connected address, for example someone@gmail.com |
Response 200
| sent_to | string | |
| forwarding_address | string |
/api/emails/{address}/connectorRead the connection of a connected mailbox
Kind, status, the import bound, the sync phase (the import scan, then history), the import progress (scanned, copied, to analyse, analysed), the last sync and the last error. Requires email:read. The forwarding confirmation (Gmail's code and link) is authentication mail: without email:auth_secrets its code and url are null and it carries redacted: true. A mailbox-confined token finds only its own mailboxes (404 otherwise).
Parameters
| address* | string | The connected address, for example someone@gmail.com |
Response 200
| address | string | |
| mailbox_id | string | |
| kind | string | |
| status | string | |
| import_since | string (date-time) | null | The bound of the first import: no mail received before it is copied |
| phase | string | list while the import scan pages through the mail after the bound, history once new mail follows the source's history |
| paused_reason | string | null | |
| import_run | object | null | The current or last import run of this mailbox |
| source_messages_total | integer | null | What the source reports for the whole inbox, for scale |
| last_sync_at | string (date-time) | null | |
| last_error | string | null | |
| backoff_until | string (date-time) | null | |
| verified_at | string (date-time) | null | |
| connected_at | string (date-time) | |
| cost_cap_usd | number | The model spend allowed per import run, in USD |
| access | string | null | For a Gmail connection: what the grant allows; write means labels both ways and sending as the person |
| forwarding | object | null | For a connection by forwarding: the forwarding address, what arrived, Gmail's confirmation, the probe, the steps |
/api/emails/{address}/connectorDisconnect a connected mailbox
Removes the person's own copies (messages, attachments, search rows, pending jobs) and the credential; keeps the mailbox row, the link marked disconnected, and what agents did (receipts). Only a connected mailbox can be disconnected. Requires email:write.
Parameters
| address* | string |
Response 200
| disconnected | boolean | |
| address | string | |
| removed_messages | integer | |
| removed_attachments | integer |
/api/emails/{address}/importImport older mail of a connected mailbox
Widens the import to an earlier date: a new import run copies the mail between the new bound and the current one, in arrival order, under its own cost cap. Refused while an import scan is still running. Requires email:write.
Parameters
| address* | string |
Request body
| since* | string (date-time) | The new bound, earlier than the current one |
Response 200
| import_run_id | string | |
| import_since | string (date-time) | |
| import_until | string (date-time) |
/api/emails/{address}/searchSearch one mailbox by meaning and by words
The same search as GET /api/emails/search, restricted to this mailbox. Requires email:read. Without email:auth_secrets, inbound authentication mail is left out before ranking: no word, operator or filter matches it, so a search for a sign-in code finds nothing.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| q* | string | What you are looking for, in any language: a name, a reference, or a description of the mail, plus Gmail operators (`from:dave is:unread newer_than:7d -invoice "exact phrase"`). |
| mode | string(hybrid | semantic | keyword) | hybrid fuses the embedding and the words by rank; semantic uses the meaning only; keyword the words only. |
| limit | integer | Results to return. |
| since | string | Only mail received after this ISO timestamp. |
| until | string | Only mail received before this ISO timestamp. |
| folder | string(inbox | archive | sent | spam | trash | anywhere) | One folder. Trash is excluded unless named or `anywhere`. |
| from | string | Sender address contains. |
| label | string | Comma-separated labels: only messages carrying any of them |
Response 200
| results | object[] | |
| mode | string | |
| query | string | |
| took_ms | integer | |
| hint | string |
/api/emails/{address}/threadsList the conversations of a mailbox
Messages grouped the way mail clients group them: a reply joins the thread of the message it answers (In-Reply-To, then the References chain), mail without usable headers joins the most recent thread with the same subject and a correspondent in common within thirty days, everything else starts a thread whose id is its first message's id. Threads are ordered by last activity; trash and spam never take part unless named as the folder. folder=inbox|archive|sent keeps the threads that have a message there but still counts the whole conversation.
Parameters
| address* | string | |
| folder | string(all | inbox | archive | sent | spam | trash) | |
| since | string | Only threads with activity after this time |
| limit | integer | |
| cursor | string | next_cursor from the previous page |
| label | string | Comma-separated labels: only threads with a message carrying any of them |
Response 200
| threads | EmailThread[] | |
| next_cursor | string | null | |
| has_more | boolean |
/api/emails/{address}/threads/{id}Read one conversation, oldest message first
Every message of the thread (trash included, so nothing is missing from the story) with its text, oldest first, plus the participants and the unread count. Auth mail is redacted the way message reads are.
Parameters
| address* | string | |
| id* | string | Thread id (the thread_id of any of its messages) |
Response 200
| id | string | |
| subject | string | null | |
| participants | string[] | |
| message_count | integer | |
| unread_count | integer | |
| first_message_at | string (date-time) | |
| last_message_at | string (date-time) | |
| messages | object[] |
/api/emails/{address}/eventsStream mailbox events (server-sent events)
Answers text/event-stream. Without a cursor the stream starts at the present; with since (a cursor from /api/email/changes or the id of any earlier event) it first replays every change after that point, then follows the mailbox journal live: each frame carries the change's type as the event name (message.received, message.updated, message.deleted, message.moved, message.status, mailbox.resync_required, assistant.item.delegated, assistant.item.recalled, ...), the change as JSON data with its resumable cursor, and that same cursor as the frame id. The first frame is ready with the cursor to resume from; a keep-alive comment arrives every 20 seconds; the connection closes after about 5 minutes with a close frame and a retry: hint. Reconnect with Last-Event-ID (or since) to resume with no gap and no duplicate. Agent tokens are confined to the mailboxes the agent operates. An entry about a work item made from authentication mail carries its title and ask redacted for a token without email:auth_secrets.
Parameters
| address* | string | |
| since | string | Cursor to replay from; the `cursor` of any earlier event or a next_cursor from /api/email/changes |
/api/emails/{address}/foldersList email folders and counts
Return server-derived counts for system folders and virtual views, plus lifecycle capabilities for the mailbox backend.
Parameters
| address* | string |
Response 200
| folders | object[] | |
| views | object[] | |
| capabilities | object |
/api/emails/{address}/messages/actionsApply a lifecycle action to messages
Move, restore, mark read, star, or permanently delete up to 100 explicit message IDs. Moving to Trash is reversible. Permanent deletion requires email:delete and only accepts messages already in Trash. Every response is a durable operation receipt with one result per requested ID. Supply a stable key in the standard Idempotency-Key header or legacy idempotency_key JSON field and reuse it after timeouts; an exact retry returns the stored receipt without another mutation. If both forms are supplied, they must match. Reusing a key with a different request returns 409. When omitted, the server creates a key and returns it in the body and Idempotency-Key header. Returns 207 when only part of the batch succeeds.
Parameters
| address* | string |
Request body
| message_ids* | string[] | |
| action* | string(move | mark_read | star | restore | delete_permanently | label) | |
| destination | string(inbox | archive | sent | spam | trash) | |
| read | boolean | |
| starred | boolean | |
| add | string[] | label: labels to add |
| remove | string[] | label: labels to remove |
| set | string[] | label: replace every label (wins over add and remove) |
| idempotency_key | string | Stable key for this exact logical batch. Reuse after a timeout. |
Response 200
| operation_id | string | |
| idempotency_key | string | |
| idempotent_replay | boolean | True when this response reused the immutable completed receipt and performed no new mutation. |
| action | string | |
| status | string | |
| results | object[] | |
| summary | object | |
| created_at | string (date-time) | |
| completed_at | string (date-time) | null |
/api/emails/{address}/messages/readMark messages as read or unread
Compatibility endpoint for retry-safe read-state updates. Returns the canonical durable per-item operation receipt plus the legacy count. Supply and reuse the Idempotency-Key header or compatibility idempotency_key field after timeouts.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Request body
| message_ids* | string[] | IDs of messages to mark |
| read* | boolean | true to mark as read, false to mark as unread |
| idempotency_key | string |
Response 200
| operation_id | string | |
| idempotency_key | string | |
| idempotent_replay | boolean | True when this response reused the immutable completed receipt and performed no new mutation. |
| action | string | |
| status | string | |
| results | object[] | |
| summary | object | |
| created_at | string (date-time) | |
| completed_at | string (date-time) | null | |
| count | integer |
/api/emails/{address}/messages/deleteBulk delete messages
Compatibility endpoint that retry-safely moves messages to Trash. Returns the canonical durable per-item operation receipt plus the legacy count. Supply and reuse the Idempotency-Key header or compatibility idempotency_key field after timeouts.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Request body
| message_ids* | string[] | IDs of messages to delete |
| idempotency_key | string |
Response 200
| operation_id | string | |
| idempotency_key | string | |
| idempotent_replay | boolean | True when this response reused the immutable completed receipt and performed no new mutation. |
| action | string | |
| status | string | |
| results | object[] | |
| summary | object | |
| created_at | string (date-time) | |
| completed_at | string (date-time) | null | |
| count | integer |
/api/emails/{address}/messages/{id}Get a message
Fetch a single message by ID from a mailbox identified by its full email address. Messages classified as authentication mail (OTP, password reset, magic link) come back with subject/body redacted unless the token has the email:auth_secrets scope.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| id* | string | Message ID |
Response 200
| id | string | |
| thread_id | string | Conversation id shared by every message of the thread (the root message id) |
| labels | string[] | Every label on the message; `label` is the first one |
| thread_key | string | Canonical backend-namespaced conversation key |
| thread_aliases | string[] | Legacy workflow keys to pass back when mutating conversation state |
| message_id | string | null | |
| direction | string | |
| from | string | The sender's display name and address from the From header (e.g. 'Alice <alice@example.com>'), or the bare address when the mail carried no name. |
| avatar_url | string (uri) | null | Privacy-preserving Domani proxy URL for the sender avatar |
| to | string | Every To recipient from the header, with display names, in the order the sender wrote them (not only the envelope address that routed this mail to the mailbox). |
| cc | string | null | Every Cc recipient from the header, with display names, or null when the mail carried no Cc. |
| bcc | string | null | Bcc recipients, present only on your own outbound message (never on one you received, and never delivered as a header). |
| reply_to | string | null | |
| subject | string | |
| text | string | null | |
| html | string | null | |
| in_reply_to | string | null | |
| references | string | null | |
| attachments | object[] | |
| status | string | |
| is_read | boolean | |
| read_at | string | null | |
| events | object[] | |
| created_at | string (date-time) |
/api/emails/{address}/messages/{id}Delete a message
Move a message to Trash. This is reversible. Use the lifecycle action with delete_permanently and an email:delete token to erase a message already in Trash.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| id* | string | Message ID |
Response 200
| id | string | |
| deleted | boolean |
/api/emails/{address}/messages/{id}/replyReply to a message
Reply to a message in a mailbox identified by its full email address. Threading headers are auto-populated, and the original is quoted under the reply as mail clients do (attribution line in the account's language, > lines in text, a gmail_quote block in HTML) unless quote is false. Collaborative clients should pass the compose lease, observed reply_version, and a stable idempotency key; the server atomically rejects a stale/double reply immediately before sending.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| id* | string | Message ID to reply to |
Request body
| text | string | Plain text reply body |
| html | string | HTML reply body |
| cc | any | Additional CC recipient(s) |
| all | boolean | Reply-all: auto-populate CC from original To/CC |
| quote | boolean | Quote the original under the reply; false sends only the text or html given |
| idempotency_key | string | Body compatibility form of Idempotency-Key |
| coordination | object |
Response 200
| id | string | |
| message_id | string | |
| from | string | |
| to | string | |
| status | string | |
| reply_version | integer | Reserved generation for a guarded reply |
/api/emails/{address}/messages/{id}/forwardForward a message
Forward a message from a mailbox identified by its full email address to another recipient. Forwarding authentication mail (OTP, password reset, magic link) requires the email:auth_secrets scope - it would exfiltrate the secret.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| id* | string | Message ID to forward |
Request body
| to* | string | Recipient email address |
| text | string | Optional note to prepend |
Response 200
| id | string | |
| message_id | string | |
| from | string | |
| to | string | |
| status | string |
/api/emails/{address}/avatarGet mailbox avatar info
Returns the Gravatar URL (if set) and signup link for a mailbox identified by its full email address.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| address | string | |
| gravatar_url | string | null | |
| gravatar_signup | string | |
| has_avatar | boolean | |
| hint | string |
/api/emails/{address}/webhookGet mailbox webhook config
Returns the webhook URL, signing secret, configured auth header names, and status for a mailbox. Header values are never returned.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| address | string | |
| webhook_url | string | null | |
| signing_secret | string | null | |
| has_webhook | boolean | |
| events | string[] | Events this webhook carries: email.received, any delivery verdict on outbound mail (email.delivered, email.bounced, email.failed, email.suppressed, email.complained, email.delayed) and the agent hand-over notices (assistant.item.delegated, assistant.item.recalled); empty without a webhook |
| header_names | string[] | Configured auth header names; values are never returned |
| hint | string |
/api/emails/{address}/webhookSet mailbox webhook
Configure the HTTPS destination for HMAC-signed mailbox events: inbound mail by default, plus (opt-in via events) the delivery verdict on mail sent FROM the mailbox - a bounce reaches the sender seconds after the send was accepted, instead of living only in the message trail. Live inbound deliveries are stored durably, attempted immediately, and retried up to three times. Each attempt includes X-Domani-Delivery-Attempt starting at 1. Optional Authorization or X-API-Key values are encrypted and never returned; human operators should submit them outside model context through the CLI environment-variable options.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Request body
| url* | string | HTTPS webhook URL |
| events | string[] | Events to push (assistant.item.delegated and assistant.item.recalled tell an agent that the owner handed it a Today item or took it back; the webhook of every mailbox the agent operates receives them). Omitted keeps the current subscription (inbound only for a new webhook). Delivery verdicts on mail sent FROM the mailbox are opt-in and arrive at the same URL with the same signature: {event, mailbox, email, message: {id, message_id, to, subject, status, detail}}. |
| headers | object | Optional encrypted auth headers. Only Authorization and X-API-Key are accepted; send {} to clear. Do not pass values through model context; the CLI supports --authorization-env and --api-key-env. |
Response 200
| address | string | |
| webhook_url | string | |
| signing_secret | string | |
| has_webhook | boolean | |
| events | string[] | Events this webhook carries: email.received, any delivery verdict on outbound mail (email.delivered, email.bounced, email.failed, email.suppressed, email.complained, email.delayed) and the agent hand-over notices (assistant.item.delegated, assistant.item.recalled); empty without a webhook |
| header_names | string[] | Configured auth header names; values are never returned |
| hint | string |
Example
curl -X PUT https://domani.run/api/emails/agent%40example.com/webhook \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://agent.example/webhook","events":["email.received","email.bounced","email.delivered"]}'/api/emails/{address}/webhookRemove mailbox webhook
Remove the webhook URL and signing secret from a mailbox. Inbound emails will only be stored.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| address | string | |
| webhook_url | string | null | |
| signing_secret | string | null | |
| has_webhook | boolean | |
| events | string[] | Events this webhook carries: email.received, any delivery verdict on outbound mail (email.delivered, email.bounced, email.failed, email.suppressed, email.complained, email.delayed) and the agent hand-over notices (assistant.item.delegated, assistant.item.recalled); empty without a webhook |
| header_names | string[] | |
| hint | string |
/api/emails/{address}/webhook/rotateRotate webhook signing secret
Regenerate the HMAC-SHA256 signing secret for a mailbox webhook. The old secret is immediately invalidated.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| address | string | |
| webhook_url | string | |
| signing_secret | string | |
| has_webhook | boolean | |
| hint | string |
/api/emails/{address}/webhook/testTest mailbox webhook
Send one signed test payload to the mailbox webhook URL. Returns HTTP status and whether delivery succeeded. Tests are not retried; live inbound deliveries are retried up to three times.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| address | string | |
| webhook_url | string | |
| status | integer | |
| success | boolean | |
| hint | string |
/api/emails/{address}/aliasesList mailbox aliases
List all alias addresses that deliver into a mailbox. Aliases let several addresses (sales@, hello@, contact@) share one inbox without using a mailbox slot.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| mailbox | string | |
| aliases | object[] | |
| hint | string |
/api/emails/{address}/aliasesAdd a mailbox alias
Add an alias address that delivers into this mailbox, without using a mailbox slot. The alias must be on the same domain as the mailbox. Provide a bare slug ("sales") or a full address ("sales@mysite.com").
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Request body
| alias* | string | Alias address - a bare slug or full slug@domain on the same domain |
Response 201
| address | string | |
| mailbox | string | |
| created_at | string (date-time) | |
| hint | string |
/api/emails/{address}/aliases/{alias}Remove a mailbox alias
Remove an alias from a mailbox. Email to that address stops being delivered.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| alias* | string | Alias address to remove (URL-encoded, or a bare slug) |
Response 200
| address | string | |
| deleted | boolean | |
| hint | string |
/api/emails/{address}/client-settingsGet mail client settings (hosted)
Get IMAP/SMTP connection settings for a hosted mailbox, to configure Apple Mail, Thunderbird, or any mail client. Username is the full address; the password is an app password (create one with POST /api/emails/{address}/credentials). Hosted mailboxes only.
Parameters
| address* | string | Hosted mailbox address (URL-encoded) |
Response 200
| mailbox | string | |
| username | string | |
| imap | object | |
| smtp | object | |
| hint | string |
/api/emails/{address}/credentialsList app passwords (hosted)
List the app passwords for a hosted mailbox. Secrets are shown only once at creation; this returns labels and metadata only. Hosted mailboxes only.
Parameters
| address* | string | Hosted mailbox address (URL-encoded) |
Response 200
| mailbox | string | |
| credentials | object[] | |
| hint | string |
/api/emails/{address}/credentialsCreate an app password (hosted)
Create an app password for a hosted mailbox, used as the password in a mail client (IMAP/SMTP). The secret is returned once - store it now. Hosted mailboxes only.
Parameters
| address* | string | Hosted mailbox address (URL-encoded) |
Request body
| label | string | Label to identify this app password, e.g. "Laptop Mail" |
Response 201
| id | string | |
| label | string | |
| password | string | The app password - shown once, store it now |
| client_settings | object | |
| hint | string |
/api/emails/{address}/credentials/{id}Revoke an app password (hosted)
Revoke an app password. Any mail client using it stops connecting immediately. Hosted mailboxes only.
Parameters
| address* | string | Hosted mailbox address (URL-encoded) |
| id* | string | App password id (from the list endpoint) |
Response 200
| id | string | |
| revoked | boolean | |
| hint | string |
/api/emails/{address}/messages/{id}/escalateFlag a message for a human
Escalate a message to the account's human (or clear the flag with clear=true). Sets needs_human on the message, holds its conversation in status needs_human, and fires the email.escalated webhook + in-app notification. The product primitive behind 'answer the easy ones, stop on the one the human must decide'.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| id* | string | Message ID to flag |
Request body
| reason | string | Why the human is needed - shown in the notification |
| clear | boolean | Clear the flag instead of setting it (the human took over) |
Response 200
| id | string | |
| needs_human | boolean | |
| escalation_reason | string | null | |
| thread_key | string |
/api/emails/{address}/messages/{id}/remindSet a follow-up reminder
Set (or clear with clear=true) a one-shot follow-up reminder on a message. When remind_at arrives and the message is still unanswered (inbound: no reply sent; outbound: no inbound reply received), email.reminder_due fires as a webhook + in-app notification and the reminder clears itself.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| id* | string | Message ID to watch |
Request body
| remind_at | string (date-time) | ISO 8601 time to be reminded (1 minute to 90 days out) |
| note | string | Context shown when the reminder fires |
| clear | boolean | Clear a pending reminder |
Response 200
| id | string | |
| remind_at | string | null | |
| reminder_note | string | null |
/api/contactsList contacts
The account's address book, auto-fed by mail traffic: correspondents, message counts, last activity, and notes. Most recently active first by default (order=recent), for display: a contact that gets new mail during a paged scan moves ahead of the cursor, so that scan can skip it. To sync the whole book, pass order=created: oldest first by creation time then id, a key that never changes, so every contact that exists for the whole scan comes back exactly once and contacts created meanwhile come at the end. A cursor only continues the order it came from (400 INVALID_CURSOR otherwise). Aggregates data from every mailbox, so mailbox-confined tokens are denied (400 MAILBOX_CONFINED).
Parameters
| q | string | Filter by address or name substring |
| limit | integer | Contacts to return (default 50) |
| cursor | string | Pagination cursor from a previous response |
| order | string(recent | created) | Sort order. recent (default): most recently active first, for display. created: oldest first by creation time then id, stable for a full scan. Any other value is 400 INVALID_ORDER. |
Response 200
| contacts | object[] | |
| next_cursor | string | null |
/api/contactsCreate or update a contact
Save a contact: set a display name and attach notes ('prefers short emails', 'decision maker at Acme'). Mail traffic keeps feeding message counts either way.
Request body
| address* | string | The contact's email address |
| name | string | null | Display name (null clears it) |
| notes | string | null | Notes agents and humans read for context (null clears them) |
/api/contacts/{address}Delete a contact
Remove a contact from the address book. Mail traffic will recreate it as an auto contact if they write again.
Parameters
| address* | string | The contact's email address (URL-encoded) |
/api/emails/{address}/rulesList inbound mail rules
List a mailbox's inbound filtering rules, in the priority order they are applied (first match wins).
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Response 200
| mailbox | string | |
| rules | MailRule[] | |
| hint | string |
/api/emails/{address}/rulesAdd an inbound mail rule
Add a rule that runs on inbound messages matching a field (from/to/subject/body). Actions: drop, mark_read, forward (action_arg = address), webhook_only (skip forward-to), label (action_arg = label). Rules apply in priority order (lower first); first match wins.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
Request body
| match_field* | string(from | to | subject | body) | |
| match_op* | string(contains | equals | regex) | |
| match_value* | string | |
| action* | string(drop | mark_read | forward | webhook_only | label | escalate) | |
| action_arg | string | Forward address (for forward), label value (for label), or reason shown to the human (for escalate) |
| priority | integer | Lower runs first (default 0) |
| enabled | boolean | Default true |
Response 201
| id | string | |
| priority | integer | Lower runs first |
| match_field | string | |
| match_op | string | |
| match_value | string | |
| action | string | |
| action_arg | string | null | |
| enabled | boolean | |
| created_at | string (date-time) |
/api/emails/{address}/rules/{ruleId}Remove an inbound mail rule
Remove an inbound filtering rule from a mailbox.
Parameters
| address* | string | Mailbox email address (URL-encoded) |
| ruleId* | string | The rule ID |
Response 200
| id | string | |
| deleted | boolean | |
| hint | string |
/api/domains/{domain}/email/catch-allSet domain catch-all
Route every email sent to an unmatched address on the domain into a designated mailbox. Anything that does not match a mailbox, subaddress, or alias lands in the catch-all. The mailbox must already exist on the domain. It changes mail for the whole domain, so a mailbox-confined token gets 403 MAILBOX_CONFINED.
Parameters
| domain* | string | Domain name |
Request body
| mailbox* | string | Catch-all mailbox - a bare slug or full address on this domain |
Response 200
| domain | string | |
| catch_all | string | null | |
| hint | string |
/api/domains/{domain}/email/catch-allClear domain catch-all
Remove the catch-all on a domain. Email to unmatched addresses will be dropped again. It changes mail for the whole domain, so a mailbox-confined token gets 403 MAILBOX_CONFINED.
Parameters
| domain* | string | Domain name |
Response 200
| domain | string | |
| catch_all | string | null | |
| hint | string |
/api/email/changesIncrementally synchronize one mailbox
Call without a cursor to capture a baseline before a bounded full snapshot. Then pass next_cursor to replay mutations that happened during and after that snapshot. Cursors are opaque, signed, mailbox-bound, and expire after 30 days. Push notifications are only invalidation hints; this journal is the durable recovery source. An entry about a work item made from authentication mail carries its title and ask redacted for a token without email:auth_secrets.
Parameters
| mailbox_id* | string | |
| cursor | string | Opaque next_cursor from a prior response |
| limit | integer |
Response 200
| changes | MailboxChange[] | |
| next_cursor | string | |
| has_more | boolean | |
| requires_full_sync | boolean | |
| reason | string |
/api/suppressionsList suppressed addresses
List addresses on your suppression list - hard bounces and complaints (ingested automatically from delivery events) plus manual entries. Sends to these addresses are skipped. Cursor-paginated.
Parameters
| cursor | string | Pagination cursor from a previous response |
| limit | integer | Max entries (default 50, max 100) |
Response 200
| suppressions | object[] | |
| next_cursor | string | null | |
| hint | string |
/api/suppressionsAdd a suppression
Manually add an address to your suppression list so future sends skip it. Hard bounces and complaints are added automatically; use this for addresses you want to stop emailing. Idempotent.
Request body
| address* | string | Email address to suppress |
| reason | string(manual | hard_bounce | complaint) | Reason (default: manual) |
Response 201
| address | string | |
| reason | string | |
| source | string | |
| created_at | string (date-time) | |
| hint | string |
/api/suppressions/{address}Remove a suppression
Remove an address from your suppression list so you can email it again (e.g. after the recipient fixed their mailbox).
Parameters
| address* | string | Suppressed email address (URL-encoded) |
Response 200
| address | string | |
| deleted | boolean | |
| hint | string |
Collaboration
/api/workspaces/{id}/ownership-transferRequest a workspace ownership transfer
Owner-only. Sends a 48-hour acceptance link to an existing member. Nothing moves until that authenticated member accepts.
Parameters
| id* | string |
Request body
| membership_id* | string |
/api/workspaces/{id}/ownership-transfer/{transferId}Revoke a pending ownership transfer
Parameters
| id* | string | |
| transferId* | string |
/api/workspaces/ownership-transfers/acceptAccept workspace ownership
Target-only. Atomically transfers the workspace, scoped domains and mailboxes; the previous owner becomes an admin and the existing payer remains unchanged.
Request body
| token* | string |
Authentication
/api/auth/registerPublicCreate a new account
Starts creating a new account (or signs an existing one in) by emailing a sign-in link; no API key is returned here. A person clicking that link in a browser lands on the dashboard already signed in. A CLI or agent without a browser session should use the device-login flow instead (POST /api/auth/cli then poll GET /api/auth/cli/poll), which emails the same approval and hands back an API key once approved - the response's next_steps says so. Pass a ref code to permanently link the new user to a referrer - the referrer earns a commission on all future domain purchases by this user.
Request body
| email* | string (email) | |
| ref | string | Referral code - permanently links this user to the referrer. The referrer earns a commission on every domain purchase. |
Response 202
| ok | boolean | |
| verification_required | boolean | |
| message | string | |
| hint | string | |
| next_steps | string[] |
Example response
{
"ok": true,
"verification_required": true,
"message": "Check your email for a sign-in link to finish creating your account.",
"hint": "A human must open that email and click the link in a browser to finish verification; no API key is issued yet.",
"next_steps": [
"If a person can click the email link, nothing else to do: they land on the dashboard already signed in.",
"If you are a CLI or agent without a browser session, start a device-login flow instead: POST /api/auth/cli, then poll GET /api/auth/cli/poll?code=... until status is \"complete\" - the same email approves it and hands back an API key, no browser needed on your side."
]
}Example
curl -X POST https://domani.run/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'/api/auth/loginPublicSend a magic link sign-in email
Sends a sign-in link to the provided email. If the user doesn't exist, creates an account first. The magic link redirects to the dashboard with auth credentials. For programmatic API access, use POST /api/auth/register to get an API key directly.
Request body
| email* | string (email) |
Response 200
| ok | boolean | |
| message | string |
Example response
{
"ok": true,
"message": "Check your email for a sign-in link."
}Example
curl -X POST https://domani.run/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'/api/auth/verifyPublicVerify a magic link token
Validates the magic link token and redirects to the dashboard with auth credentials. Used by the magic link email - not typically called by agents directly. Agents should use POST /api/auth/register instead.
Parameters
| token* | string | Magic link token from email (base64url-encoded) |
/api/auth/handoffMint a one-time cross-host sign-in code
Browser-only: lets a signed-in person on domani.run or mailzero.run resume their session on the other host without a second magic link. Returns a URL whose fragment carries a 60-second, single-use, host-bound code; the code is never logged or sent in a query string. Not useful outside a browser session, so it is not exposed as an agent tool.
Request body
| target* | string(mail | domani) | |
| path* | string |
Response 200
| url | string |
Example response
{
"url": "https://mailzero.run/auth/handoff#code=xxx&next=%2Finbox"
}/api/auth/handoff/redeemPublicRedeem a one-time cross-host sign-in code
Browser-only: called by the /auth/handoff page on the target host to exchange the code from the URL fragment for a session, exactly like GET /api/auth/verify does for a magic link. The code must match the Host this request arrived on. Not an agent tool.
Request body
| code* | string |
Response 200
| ok | boolean | |
| next | string |
Example response
{
"ok": true,
"next": "/inbox"
}Account
/api/meGet current account details
Returns the authenticated user's account information including email, payment status, contact info status, referral code, and usage counts. Agents should call this after authentication to check has_payment_method and has_contact before attempting purchases.
Response 200
| id | string | |
| string (email) | ||
| plan | string | Active personal Domani plan key |
| workspace_plans | string[] | Distinct active Mailzero plan keys on workspaces the user can access |
| has_payment_method | boolean | Whether the user has a card on file (required before buying domains) |
| preferred_payment_method | string | null | |
| preferred_locale | string | Locale used by Domani interfaces and transactional communications |
| has_contact | boolean | Whether the user has set WHOIS contact info. Optional for purchases (until it is set, Domani's contact holds new domains on the user's behalf), required before transferring a domain in. |
| contact | RegistrantContact | Registrant contact info (null if not set) |
| referral_code | string | Your unique referral code |
| referral_rate | integer | Commission percentage on referred purchases |
| balance | number | Marketplace credit balance in USD (earned from sales) |
| domain_count | integer | Number of domains owned |
| token_count | integer | Number of active API tokens |
| features | string[] | Feature flags enabled for this account (GA features plus the preview features granted to the user), e.g. mailzero_assistant |
| can_manage_assistant | boolean | Whether the calling token has the assistant:read scope. Independent of `features`: a feature flag says the account is eligible for the assistant, this says whether this specific token (full access, or a scoped CLI/OAuth/delegated token) may read or change its settings. |
| is_admin | boolean | |
| mailbox_entitlement | object | Custom-domain mailbox room on the account's personal workspace. Its own plan, not `plan` above: a workspace can carry its own Mailzero subscription while the user's own plan stays free. |
| created_at | string (date-time) |
Example response
{
"id": "cm5abc123",
"email": "dev@example.com",
"plan": "agent",
"workspace_plans": [
"mail_pro"
],
"has_payment_method": true,
"has_contact": true,
"referral_code": "x7k9m2",
"referral_rate": 20,
"domain_count": 3,
"token_count": 2,
"created_at": "2025-01-15T10:30:00.000Z"
}Example
curl https://domani.run/api/me \ -H "Authorization: Bearer domani_sk_xxx"
/api/meUpdate account preferences
Update the authenticated user's preferred payment method and/or BCP 47 locale. API responses remain structured and are not translated.
Request body
| preferred_payment_method | string | null(card | usdc) | Default payment method for purchases (card, usdc, or null for auto-select) |
| preferred_locale | string(en | fr | es | pt-BR | ja | zh-Hans | it) | Locale used by Domani interfaces and transactional communications |
Response 200
| preferred_payment_method | string | null | |
| preferred_locale | string | |
| hint | string |
/api/meDelete account (blocked while domains are held)
Permanently delete the authenticated user's account, tokens, webhooks, mailboxes, and other application data. Registered domains are NOT deleted with the account: the request fails with 409 HAS_DOMAINS until every domain is transferred out. This action cannot be undone.
Response 200
| deleted | boolean | |
| hint | string |
/api/me/activationGet truthful activation state and recommended next actions
Returns server-derived product milestones and capability-aware next actions. Token creation, token use, page views, and client checklist state are deliberately excluded.
Response 200
| version | integer | |
| account_created_at | string (date-time) | |
| entry | object | |
| milestones | object | |
| status | string | |
| recommended_actions | object[] | |
| available_paths | string[] | |
| dismissed_version | integer | |
| pending_claim | object | null |
Example
curl https://domani.run/api/me/activation \ -H "Authorization: Bearer domani_sk_xxx"
/api/me/activationDismiss or restore first-run guidance
Stores presentation preference only. It cannot complete a product milestone.
Request body
| dismissed_version* | integer |
Response 200
| dismissed_version | integer | |
| hint | string |
/api/me/contactGet registrant contact info
Returns the authenticated user's WHOIS registrant contact information. Optional for purchases (until it is set, Domani's contact holds new domains on the user's behalf), required before transferring a domain in.
Response 200
| has_contact | boolean | |
| contact | RegistrantContact |
Example response
{
"has_contact": true,
"contact": {
"first_name": "Jane",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US",
"phone": "+1.2125551234",
"email": "jane@example.com"
}
}Example
curl https://domani.run/api/me/contact \ -H "Authorization: Bearer domani_sk_xxx"
/api/me/contactSet registrant contact info
Set or update your WHOIS registrant contact information. Optional for purchases, required before transferring a domain in. It becomes the registrant and billing contact of your domains: until it is set, Domani's contact holds them on your behalf. When updated, the new contact is pushed to all your active domains at registrars that support contact updates, which makes you their registrant; the registrar may email you to confirm it within 15 days.
Response 200
| contact | RegistrantContact |
Example
curl -X PUT https://domani.run/api/me/contact \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"first_name":"Jane","last_name":"Doe","address1":"123 Main St","city":"New York","state":"NY","postal_code":"10001","country":"US","phone":"+1.2125551234","email":"jane@example.com"}'/api/me/resend-verificationResend contact email verification
Resend the verification email for the account's contact email address. Rate limited to once per 15 minutes per email.
Request body
| string (email) | Email address to verify (defaults to account contact email) |
Response 200
| sent | boolean | |
| already_verified | boolean | |
| hint | string |
/api/sessionsList signed-in devices
Every browser, mobile app and terminal signed in to the account, the caller's own session first (current). Each sign-in opens its own session, so signing in on a new device never signs another one out. Location is where the device signed in from (country and city from its IP), absent when unknown. API keys are not sessions: list them with GET /api/tokens.
Response 200
| sessions | object[] | |
| hint | string |
Example response
{
"sessions": [
{
"id": "cm5ses123",
"current": true,
"kind": "web",
"product": "domani",
"device": {
"type": "desktop",
"os": "macOS",
"browser": "Chrome"
},
"location": {
"country": "FR",
"city": "Paris"
},
"signed_in_at": "2026-10-01T10:00:00.000Z",
"last_active_at": "2026-10-18T09:12:00.000Z"
},
{
"id": "cm5ses456",
"current": false,
"kind": "mobile",
"product": null,
"device": {
"type": "mobile",
"os": "iOS",
"browser": null
},
"location": {
"country": "FR",
"city": "Lyon"
},
"signed_in_at": "2026-09-20T18:30:00.000Z",
"last_active_at": "2026-10-17T21:04:00.000Z"
}
],
"hint": "Sign a device out with DELETE /api/sessions/{id}, or every device but this one with DELETE /api/sessions."
}Example
curl https://domani.run/api/sessions \ -H "Authorization: Bearer domani_sk_xxx"
/api/sessionsSign out every other device
Signs out every session of the account except the one making the request. Those devices are asked to sign in again on their next request. API keys are unchanged.
Response 200
| revoked | integer | |
| hint | string |
Example response
{
"revoked": 2,
"hint": "Every other device is signed out. API keys are unchanged."
}Example
curl -X DELETE https://domani.run/api/sessions \ -H "Authorization: Bearer domani_sk_xxx"
/api/sessions/{id}Sign out a device
Signs one session out. current names the session making the request (signing out needs no scope); any other session needs tokens:write.
Parameters
| id* | string | Session id (from GET /api/sessions), or `current` |
Response 200
| success | boolean | |
| current | boolean | True when the caller signed itself out: its key no longer works |
| hint | string |
Example response
{
"success": true,
"current": false,
"hint": "That device is signed out and will be asked to sign in again."
}Example
curl -X DELETE https://domani.run/api/sessions/cm5ses456 \ -H "Authorization: Bearer domani_sk_xxx"
API Tokens
/api/agents/identity/{slug}/tokensList the API tokens of an agent identity
The tokens bound to this agent identity (masked keys, scopes, mailbox confinement, last use). An agent-bound token is that agent's view on every assistant and mail surface.
Parameters
| slug* | string | The agent identity handle. |
/api/agents/identity/{slug}/tokensGive an agent its own API key in one call
Creates a token bound to this agent identity, confined to the mailboxes the agent operates, with the minimal scopes an agent needs (assistant:read, assistant:write, email:read, email:write). With that key the agent reads only its own Today, its mailboxes and the items handed to it; naming another agent answers 403 and owner-only routes (settings, backfill, export) refuse it. The full key is returned only in this response. Requires tokens:write. An agent that operates no mailbox gets no mail scopes by default, and asking for them answers 400 AGENT_MAILBOXES_REQUIRED: a key with no mailboxes would reach every mailbox of the account.
Parameters
| slug* | string | The agent identity handle. |
Request body
| name | string | Defaults to '<slug> agent'. |
| scopes | string[] | Defaults to assistant:read, assistant:write, email:read, email:write. Cannot exceed the caller's own scopes. |
| mailboxes | string[] | Mailbox addresses the token is confined to. Defaults to the mailboxes the agent operates. |
| expires_in | integer | Seconds until expiry (max one year). |
Response 201
| success | boolean | |
| token | object | |
| agent | object | |
| hint | string |
Example
curl -X POST https://domani.run/api/agents/identity/dave/tokens \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"name": "Dave on the laptop"}'/api/agents/identity/{slug}/tokens/{id}Revoke an agent's API token
Revokes a token bound to this agent identity. It stops working immediately. Requires tokens:write.
Parameters
| slug* | string | |
| id* | string |
Response 200
| success | boolean | |
| hint | string |
/api/tokensList your API tokens
Returns all API tokens for the authenticated user. Keys are masked for security (first 12 + last 4 characters visible). Use this to audit active tokens.
Response 200
| tokens | TokenInfo[] |
Example response
{
"tokens": [
{
"id": "cm5abc123",
"name": "Default",
"key": "domani_sk_a...wxyz",
"lastUsed": "2025-02-18T10:30:00.000Z",
"expiresAt": null,
"expired": false,
"scopes": [
"*"
],
"createdAt": "2025-01-15T10:30:00.000Z",
"max_per_tx": null,
"max_per_month": null,
"spent_this_month": 0,
"parent_token_id": null,
"agent_identity": null
},
{
"id": "cm5def456",
"name": "CI/CD",
"key": "domani_sk_b...vwxy",
"lastUsed": null,
"expiresAt": "2025-05-01T14:00:00.000Z",
"expired": false,
"scopes": [
"domains:read",
"domains:write"
],
"createdAt": "2025-02-01T14:00:00.000Z"
}
]
}Example
curl https://domani.run/api/tokens \ -H "Authorization: Bearer domani_sk_xxx"
/api/tokensCreate a new API token
Creates a new API token. The full key is returned only in this response - store it securely. You can create multiple tokens to separate access for different applications.
Request body
| name | string | Human-readable name to identify this token |
| expires_in | integer | Token lifetime in seconds (min 3600 = 1h, max 31536000 = 1y). Omit for no expiration. |
| expires_at | string (date-time) | Absolute expiration date (ISO 8601). Alternative to expires_in. |
| scopes | string[] | Permission scopes for this token. Use ['*'] for full access (default). A token can only grant scopes it already has (scope attenuation). Scopes: domains:read (GET /api/domains, GET /api/domains/{domain}, GET /api/domains/{domain}/dns, /dnssec, /status, /email/check, /auth-code, /transfer-away, /transfer-status, /analytics), domains:write (PUT /api/domains/{domain}/dns, POST/DELETE /api/domains/{domain}/dnssec, POST /connect, POST /verify, PUT /settings, PUT /parking, PUT/DELETE /api/domains/{domain}/for-sale, POST /api/domains/import, POST /import/verify), domains:transfer (POST /api/domains/buy, POST /transfer, POST /renew (involves payment, includes marketplace purchases)), tokens:read (GET /api/tokens), tokens:write (POST /api/tokens, DELETE /api/tokens/{id}), webhooks:read (GET /api/webhooks, GET /api/webhooks/{id}/deliveries), webhooks:write (POST /api/webhooks, PATCH /api/webhooks/{id}, DELETE /api/webhooks/{id}), email:read (GET /api/emails, /api/emails/{address}, /api/emails/{address}/messages, /api/emails/{address}/aliases, /api/email/changes, /api/domains/{domain}/email/status, /api/domains/{domain}/email/deliverability, /api/suppressions, GET /api/contacts), email:write (POST /api/emails, POST /api/emails/{address}/send, POST /api/domains/{domain}/email/setup, aliases + catch-all, POST/DELETE /api/suppressions, POST/DELETE /api/contacts), email:delete (Permanently delete messages already in Trash. Moving messages to Trash only requires email:write. Grant this scope only to agents allowed to irreversibly erase email content), email:manage_mailboxes (Create mailboxes (POST /api/emails) and nothing else. The narrow admin scope for agents that may spin up addresses for new projects (partners@, launch@) without being able to read or send mail, manage domains, or touch tokens), email:auth_secrets (Read messages classified as authentication mail (OTP / verification codes, password resets, magic links). Without it, email:read still lists them but subject and body come back redacted - so a stolen agent token can't harvest 2FA codes. Grant it only to agents that genuinely need to complete logins), account:read (GET /api/me, GET /api/agents/identity), account:write (DELETE /api/me, POST /api/billing/setup, POST /api/billing/subscribe, POST /api/billing/cancel, POST /api/me/resend-verification, POST/PATCH/DELETE /api/agents/identity), billing:read (GET /api/billing/invoices), search (GET /api/domains/search, /suggest, /whois, /dns-check, GET /api/tlds), deals:read (GET /api/deals, GET /api/deals/{id}), deals:write (POST /api/domains/sell, PATCH /api/deals/{id}), notifications:read (GET /api/notifications, GET /api/notifications/count), backorders:read (GET /api/backorders, GET /api/backorders/{id}), backorders:write (POST /api/backorders, DELETE /api/backorders/{id}), assistant:read (Read Mailzero assistant settings, the Today queue, Work items, exact action previews, history-import progress, and content-minimized Activity), assistant:write (Change Mailzero assistant settings, record consent and start history imports, record human Decisions on Work items, retry imports, and export or delete derived assistant data. Never sends mail). |
| max_per_tx | number | Per-transaction spend cap in USD. Any single charge above this fails with 403 SPEND_CAP_EXCEEDED. A token can only grant caps at or below its own (CAP_ESCALATION otherwise). |
| max_per_month | number | Rolling calendar-month spend cap in USD across all paid operations made with this token. |
| agent_identity_id | string | Optional id of an agent identity owned by this account. Mail actions made with the token are attributed to that agent. |
| http_route_policy | object | Required structured authority for a scoped token with http-routes:read or http-routes:write. |
| mailboxes | string[] | Confine the token to these mailbox addresses (slug@domain). A confined token only sees and acts on those mailboxes; a confined caller can only delegate a subset of its own (400 MAILBOX_ESCALATION otherwise). |
Response 201
| id | string | |
| name | string | |
| key | string | Full API key - save this, it won't be shown again |
| expiresAt | string (date-time) | null | When this token expires (null = never) |
| scopes | string[] | Permission scopes granted to this token |
| createdAt | string (date-time) | |
| max_per_tx | number | null | Per-transaction spend cap in USD (null = uncapped) |
| max_per_month | number | null | Monthly spend cap in USD (null = uncapped) |
| mailbox_ids | string[] | Mailbox ids this token is confined to |
| http_route_policy | HttpRoutePolicy | |
| agent_identity | object | null |
Example response
{
"id": "cm5ghi789",
"name": "CI/CD Pipeline",
"key": "domani_sk_x1Y2z3A4b5C6d7E8f9G0hIjK",
"expiresAt": "2025-03-20T10:30:00.000Z",
"scopes": [
"domains:read",
"domains:write"
],
"createdAt": "2025-02-18T10:30:00.000Z",
"max_per_tx": 25,
"max_per_month": 100,
"mailbox_ids": [],
"http_route_policy": null
}Example
curl -X POST https://domani.run/api/tokens \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"name": "CI/CD Pipeline"}'/api/tokens/{id}Revoke an API token
Permanently revokes an API token. Any applications using this token will immediately lose access. This action cannot be undone.
Parameters
| id* | string | Token ID (from GET /api/tokens response) |
Response 200
| success | boolean |
Example response
{
"success": true
}Example
curl -X DELETE https://domani.run/api/tokens/cm5abc123 \ -H "Authorization: Bearer domani_sk_xxx"
/api/auditList security audit events
Returns the account's security audit trail, newest first: token lifecycle (created, revoked, escalation attempts), spend-cap and max_price denials, charges, payout requests, and blocked account deletions. Use it to monitor what your agents' tokens are doing.
Parameters
| limit | integer | Events per page |
| cursor | string | Pagination cursor (next_cursor from the previous page) |
| type | string | Filter by event type (e.g. token.created, purchase.charged, purchase.denied_cap) |
Response 200
| events | object[] | |
| next_cursor | string | null | |
| hint | string |
Example response
{
"events": [
{
"id": "evt1",
"tokenId": "cm5abc123",
"type": "purchase.charged",
"targetType": "domain",
"targetId": "mysite.com",
"metadata": {
"amountCents": 1450,
"operation": "buy",
"method": "card"
},
"createdAt": "2026-07-11T10:00:00.000Z"
},
{
"id": "evt2",
"tokenId": "cm5abc123",
"type": "purchase.denied_cap",
"targetType": "domain",
"targetId": "expensive.ai",
"metadata": {
"amountCents": 8470
},
"createdAt": "2026-07-11T09:58:00.000Z"
}
],
"next_cursor": null,
"hint": "Security-relevant events for this account."
}Example
curl "https://domani.run/api/audit?limit=50" \ -H "Authorization: Bearer domani_sk_xxx"
Webhooks
/api/webhooksList webhooks
List all webhook endpoints configured for your account. Returns URL, subscribed events, and active status for each webhook. Secrets are never returned after creation. A mailbox-confined token sees only the webhooks confined to its own mailboxes.
Response 200
| webhooks | object[] | |
| hint | string |
Example response
{
"webhooks": [
{
"id": "cm5wh001",
"url": "https://example.com/webhooks/domani",
"events": [
"domain.purchased",
"dns.updated"
],
"active": true,
"created_at": "2026-02-25T10:00:00.000Z"
}
],
"hint": "1 webhook(s)."
}Example
curl https://domani.run/api/webhooks \ -H "Authorization: Bearer domani_sk_xxx"
/api/webhooksCreate webhook
Register a new HTTPS webhook endpoint. Optional Authorization or X-API-Key values are encrypted at rest and included in every delivery. The signing secret is returned only once. Maximum 5 webhooks per account. A webhook created by a mailbox-confined token is confined to the same mailboxes: it receives no mail event from any other mailbox.
Request body
| url* | string (uri) | HTTPS URL that will receive webhook POST requests |
| events* | string[] | Event types to subscribe to. Use GET /api/webhooks/events to list available types. |
| headers | object | Optional encrypted auth headers. Only Authorization and X-API-Key are accepted. |
Response 200
| id | string | |
| url | string (uri) | |
| events | string[] | |
| secret | string | HMAC-SHA256 signing secret (shown once) |
| active | boolean | |
| header_names | string[] | Configured auth header names; values are never returned |
| created_at | string (date-time) | |
| hint | string |
Example response
{
"id": "cm5wh001",
"url": "https://example.com/webhooks/domani",
"events": [
"domain.purchased",
"dns.updated"
],
"secret": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345",
"active": true,
"created_at": "2026-02-25T10:00:00.000Z",
"hint": "Webhook created. Save the secret - it won't be shown again. Use it to verify incoming payloads with HMAC-SHA256."
}Example
curl -X POST https://domani.run/api/webhooks \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/hook","events":["domain.purchased","dns.updated"],"headers":{"Authorization":"Bearer sender_key_xxx"}}'/api/webhooks/{id}Update webhook
Update a webhook's URL, subscribed events, active status, or encrypted auth headers. Send only the fields you want to change. A mailbox-confined token reaches only the webhooks confined to its own mailboxes (404 otherwise).
Parameters
| id* | string | Webhook ID |
Request body
| url | string (uri) | New HTTPS URL |
| events | string[] | New list of event types |
| active | boolean | Set false to pause, true to resume |
| headers | object | Replace encrypted auth headers; send {} to clear. Only Authorization and X-API-Key are accepted. |
Response 200
| id | string | |
| url | string (uri) | |
| events | string[] | |
| active | boolean | |
| header_names | string[] | Configured auth header names; values are never returned |
| hint | string |
/api/webhooks/{id}Delete webhook
Delete a webhook endpoint. All pending deliveries will be cancelled. This cannot be undone. A mailbox-confined token reaches only the webhooks confined to its own mailboxes (404 otherwise).
Parameters
| id* | string | Webhook ID |
Response 200
| success | boolean | |
| hint | string |
/api/webhooks/{id}/deliveriesList webhook deliveries
Get recent delivery attempts for a webhook. Shows event type, HTTP status, number of attempts, and any errors. Useful for debugging failed deliveries. A mailbox-confined token reaches only the webhooks confined to its own mailboxes (404 otherwise).
Parameters
| id* | string | Webhook ID |
| limit | integer | Maximum number of deliveries to return |
Response 200
| deliveries | object[] | |
| hint | string |
Example response
{
"deliveries": [
{
"id": "cm5del001",
"replay_of_id": null,
"event_type": "domain.purchased",
"status": "delivered",
"http_status": 200,
"attempts": 1,
"last_error": null,
"created_at": "2026-02-25T10:05:00.000Z"
}
],
"hint": "1 delivery(ies) for this webhook."
}/api/webhooks/{id}/deliveries/{deliveryId}/replayReplay a webhook delivery
Clone and immediately redeliver an immutable webhook event. The event payload and event ID stay identical; the HMAC signature receives a fresh timestamp. A required idempotency key guarantees caller retries never send twice. A mailbox-confined token reaches only the webhooks confined to its own mailboxes (404 otherwise). One exception to an identical payload: a subject copied from authentication mail is sent redacted, since the webhook's URL may have changed since the first delivery.
Parameters
| id* | string | Webhook ID |
| deliveryId* | string | Original delivery ID |
Request body
| idempotency_key* | string |
Response 200
| delivery_id | string | |
| original_delivery_id | string | |
| event_type | string | |
| status | string | |
| http_status | integer | null | |
| attempts | integer | |
| idempotency_key | string | |
| idempotent_replay | boolean | |
| created_at | string (date-time) | |
| hint | string |
/api/webhooks/eventsList webhook event types
List all available webhook event types with descriptions. No authentication required.
Response 200
| events | object[] |
Example response
{
"events": [
{
"type": "domain.purchased",
"description": "A new domain was successfully registered"
},
{
"type": "dns.updated",
"description": "DNS records were changed for a domain"
},
{
"type": "transfer.completed",
"description": "A domain transfer completed successfully"
}
]
}Example
curl https://domani.run/api/webhooks/events
Notifications
/api/notificationsList notifications
List your in-app notifications with cursor-based pagination. Includes deal events, listing updates, and system alerts. Requires notifications:read scope. A notification written before mail subjects were redacted at write time reads with the subject of authentication mail redacted, in body and data.subject, for a token without email:auth_secrets.
Parameters
| limit | integer | Max notifications to return |
| cursor | string | Cursor for pagination (from previous response's next_cursor) |
Response 200
| notifications | object[] | |
| has_more | boolean | |
| next_cursor | string | null |
/api/notifications/countGet unread notification count
Lightweight endpoint for polling unread notification count. Requires notifications:read scope.
Response 200
| unread_count | integer |
Example response
{
"unread_count": 3
}/api/notifications/{id}Mark notification as read
Mark a single notification as read. Requires notifications:read scope.
Parameters
| id* | string | Notification ID |
Request body
| read* | boolean | Set to true to mark as read |
Response 200
| ok | boolean |
/api/notifications/read-allMark all notifications as read
Mark all unread notifications as read. Requires notifications:read scope.
Response 200
| ok | boolean | |
| count | integer | Number of notifications marked as read |
/api/notifications/preferencesGet notification preferences
Returns the user's notification preferences for each category and channel (in_app, email). Missing preferences default to enabled. Requires notifications:read scope.
Response 200
| preferences | object[] | |
| expiry_alert_days | integer[] | Days-before-expiry the user receives renewal reminders (default 30/7/1). Empty = no expiry reminders. |
/api/notifications/preferencesUpdate notification preferences
Update notification preferences for specific categories and channels. Requires notifications:read scope.
Request body
| preferences | object[] | |
| expiry_alert_days | integer[] | Days-before-expiry to receive renewal reminders. Empty array = no expiry reminders. |
Response 200
| ok | boolean |
TLDs
/api/tldsPublicList the TLDs Domani sells, with pricing
Returns the TLDs Domani sells, with registration, renewal, and transfer pricing. This is the catalog of our registrars, not every TLD that exists: some root-zone TLDs such as gov or edu cannot be registered through Domani. Each entry has a kind: tld (a TLD of the IANA root zone), second_level (an offering under one, such as co.uk), or alt_root (a name outside the root zone, which does not resolve in public DNS). alt_root entries are left out unless the kind parameter asks for them. Supports filtering by price range, searching by TLD name, sorting, and pagination via limit/offset. When paginated, includes a Link header with the next page URL. Public endpoint - no authentication required. Authenticated requests get higher rate limits (60/min vs 30/min).
Parameters
| max_price | number | Maximum registration price in USD |
| min_price | number | Minimum registration price in USD |
| sort | string(tld | price | renewal) | Sort field - 'tld' (alphabetical), 'price' (registration), or 'renewal' |
| order | string(asc | desc) | Sort order |
| search | string | Filter TLD names containing this string |
| limit | integer | Max results per page |
| offset | integer | Number of results to skip |
| kind | string | Comma-separated kinds to return: `tld`, `second_level`, `alt_root`. Default: `tld,second_level`, the entries that resolve in public DNS. `alt_root` entries (outside the IANA root zone) are returned only when asked for. |
Response 200
| tlds | TldInfo[] | |
| total | integer | Total matching TLDs (before pagination) |
| offset | integer | |
| limit | integer | null |
Example response
{
"tlds": [
{
"tld": "xyz",
"registration": 4.04,
"renewal": 4.04,
"transfer": 4.04,
"kind": "tld"
},
{
"tld": "com",
"registration": 10.88,
"renewal": 13.08,
"transfer": 13.08,
"kind": "tld"
},
{
"tld": "co.uk",
"registration": 10.75,
"renewal": 10.75,
"transfer": 10.75,
"kind": "second_level"
}
],
"total": 743,
"offset": 0,
"limit": null
}Example
curl https://domani.run/api/tlds?max_price=10&sort=price
Domain Search
/api/domains/searchPublicCheck availability and pricing for domains
Checks availability and pricing for one or more domains via RDAP + retail pricing. A name RDAP calls unregistered is then checked with the registrar: a registry premium or reserved name comes back available: false, with registry_status and a reason. Use q for a single domain (returns flat object) or domains for multiple (returns array). Supports SSE streaming via Accept: text/event-stream header - closing the connection cancels all in-flight RDAP lookups server-side. Max 200 domains per request. Public endpoint - no authentication required. Authenticated requests get higher rate limits (60/min vs 20/min).
Parameters
| q | string | Single domain including TLD (shorthand - returns flat object). Mutually exclusive with `domains`. |
| domains | string | Comma-separated list of full domain names to check (max 200). Returns array result. |
| max_price | number | Only include domains under this price (USD) |
| sort | string(price | tld) | Sort results by price or TLD name |
| source | string(all | primary | secondary) | Filter results by source: 'primary' (registrar availability only), 'secondary' (Domani marketplace listings only), or 'all' (both) |
| order | string(asc | desc) |
Response 200
| available | boolean | Whether the domain is available for registration |
| domain | string | Full domain name including TLD |
| price | number | Registration price in the specified currency |
| currency | string | ISO 4217 currency code (always USD) |
| error | boolean | True if RDAP lookup failed (availability unknown) |
| for_sale | object | Present when the domain is listed for sale (either on Domani marketplace or detected on an external aftermarket) |
| tld_kind | string | Root-zone kind of the domain's suffix: `tld`, `second_level`, or `alt_root`. An `alt_root` domain is outside the IANA root zone and does not resolve in public DNS: it is not checked, and `reason` says why. |
| registry_status | string | Set when the registry holds back a name that RDAP calls unregistered: `premium` (the registry sets its own price, see `registry_price`) or `reserved` (it cannot be registered). Such a result is `available: false`, since `POST /api/domains/buy` cannot register it, and `reason` says why. |
| registry_price | number | Present on `premium` results when the registrar quoted it: the registry's first-year price in USD. `price` stays the TLD's standard price, which does not apply to this name. |
| renewal_price | number | What each year after the first costs, in USD, at the registrar that would register the name: what renewing it is billed. Absent when unknown. On a `premium` result it is, like `price`, the TLD's standard price. |
| reason | string | Why the domain is not offered: a name outside public DNS (`tld_kind` `alt_root`), or a name the registry holds back (`registry_status`). |
Example
curl "https://domani.run/api/domains/search?q=mysite.com"
/api/domains/suggestPublicAI-powered domain suggestions with availability
Given a project description, generates full domain names with creative TLDs (e.g. codebuddy.dev, wavify.fm), checks availability, and returns only available domains with pricing. Uses an agentic loop that retries until enough available domains are found. Supports SSE streaming via Accept: text/event-stream - closing the connection cancels in-flight LLM generation and RDAP lookups server-side. Public endpoint - no authentication required.
Parameters
| prompt* | string | Project description or keywords to generate domain suggestions from (min 3 characters) |
| count | integer | Number of available domains to return (default 10) |
| exclude | string | Comma-separated list of domains to exclude from suggestions (e.g. from a previous batch) |
| tlds | string | Comma-separated preferred TLDs (without dots). Results will strongly favor these TLDs. |
| style | string(single | creative | short | brandable | keyword) | Name style preference: 'single' (one word), 'creative' (evocative/metaphorical), 'short' (3-6 chars), 'brandable' (invented), 'keyword' (descriptive). |
| lang | string(japanese | spanish | french | italian | latin | nordic | arabic | sanskrit) | Language/cultural inspiration for names. Names will draw from this language's words, aesthetics, and culture. |
Response 200
| suggestions | object[] | Available domain names with pricing |
| checked | integer | Total domains checked |
| iterations | integer | Number of LLM rounds |
Example response
{
"suggestions": [
{
"domain": "codebuddy.dev",
"price": 13,
"currency": "USD",
"renewal_price": 13
},
{
"domain": "aiforge.sh",
"price": 33,
"currency": "USD"
},
{
"domain": "devcraft.app",
"price": 13,
"currency": "USD"
}
],
"checked": 42,
"iterations": 2
}Example
curl "https://domani.run/api/domains/suggest?prompt=AI+coding+assistant"
/api/domains/dns-checkPublicFast DNS-based domain existence check
Tests if a domain name is taken across many TLDs at once using DNS queries (faster than search, no pricing). Returns 'taken' (definitely registered) and 'candidates' (potentially available) arrays. Use this to narrow down before calling search for pricing. Public endpoint - no authentication required.
Parameters
| name* | string | Domain name without TLD, e.g. 'myapp' |
| tlds* | string | Comma-separated TLDs to check, e.g. 'com,dev,ai,io' |
Response 200
| name | string | |
| taken | string[] | TLDs that are definitely registered |
| candidates | string[] | TLDs that are potentially available |
Domains
/api/domainsList all your registered domains
Returns all domains owned by the authenticated user, ordered by purchase date (newest first). Includes domain name, status, purchase date, and expiration date.
Response 200
| domains | DomainRecord[] |
Example response
{
"domains": [
{
"domain": "mysite.com",
"status": "active",
"purchasedAt": "2025-01-15T10:30:00.000Z",
"expiresAt": "2026-01-15T10:30:00.000Z",
"emailEnabled": true,
"emailVerified": true,
"emailVerifiedAt": "2025-01-15T10:35:00.000Z",
"mailboxCount": 2
},
{
"domain": "myapp.dev",
"status": "active",
"purchasedAt": "2025-02-01T14:00:00.000Z",
"expiresAt": "2026-02-01T14:00:00.000Z",
"emailEnabled": false,
"emailVerified": false,
"emailVerifiedAt": null,
"mailboxCount": 0
}
]
}Example
curl https://domani.run/api/domains \ -H "Authorization: Bearer domani_sk_xxx"
/api/agents/identityClaim a free agent identity (<handle>.domani.run)
Gives an agent a free identity at <slug>.domani.run - a live profile page, no domain purchase, instantly. The frictionless way to start; upgrade to a real owned domain later. Requires account:write.
Request body
| slug* | string | The handle - becomes <slug>.domani.run |
| name | string | Display name |
| bio | string | |
| emoji | string | Avatar emoji |
| string (email) | Public contact (e.g. your free @domani.run inbox) | |
| links | object[] |
Response 200
| id | string | Stable identity id used to bind scoped API tokens |
| slug | string | |
| url | string | The identity subdomain (canonical) |
| page_url | string | Path fallback that always works |
| name | string | null | |
| bio | string | null | |
| emoji | string | null | |
| string | null | ||
| links | object[] | |
| views | integer | |
| created_at | string (date-time) |
Example response
{
"success": true,
"id": "agent_abc123",
"slug": "myagent",
"url": "https://myagent.domani.run",
"page_url": "https://domani.run/agents/myagent",
"name": "My Agent",
"bio": "I broker domains.",
"emoji": "🤖",
"email": null,
"links": [],
"views": 0,
"created_at": "2026-07-11T00:00:00.000Z"
}Example
curl -X POST https://domani.run/api/agents/identity \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"slug": "myagent", "name": "My Agent", "bio": "I broker domains.", "emoji": "🤖"}'/api/agents/identityList your agent identities
Lists the free identities (<handle>.domani.run) you've claimed. Requires account:read.
Response 200
| identities | AgentIdentity[] |
/api/agents/identity/{slug}Update an agent identity
Update a free identity's name/bio/emoji/email/links. Requires account:write.
Parameters
| slug* | string | The handle to update |
Request body
| name | string | |
| bio | string | |
| emoji | string | |
| string | ||
| links | object[] |
Response 200
| id | string | Stable identity id used to bind scoped API tokens |
| slug | string | |
| url | string | The identity subdomain (canonical) |
| page_url | string | Path fallback that always works |
| name | string | null | |
| bio | string | null | |
| emoji | string | null | |
| string | null | ||
| links | object[] | |
| views | integer | |
| created_at | string (date-time) |
/api/agents/identity/{slug}Release an agent identity
Releases a free identity handle so it's available again. Requires account:write.
Parameters
| slug* | string | The handle to release |
Response 200
| success | boolean | |
| slug | string | |
| released | boolean |
/api/agents/provisionProvision a complete agent identity in one call
Gives an AI agent a complete internet identity in a single request: buys the domain (if not already owned), sets up email DNS, creates a mailbox, and optionally registers an authenticated webhook for inbound email and domain events. webhook_headers accepts only Authorization and X-API-Key; values are encrypted at rest and never returned. Repeating the same domain, mailbox slug, and webhook URL reuses those resources. Every webhook input is validated before a domain purchase or charge. The domain purchase is the only step that can charge and the only hard prerequisite - if it fails, the whole call fails (402 for payment, 409 if taken by another account). Email setup, mailbox creation, and webhook registration are best-effort: transient issues (e.g. DNS still propagating) are returned in warnings rather than failing the call, so the agent always gets its domain. Payment works exactly like POST /api/domains/buy (card, marketplace balance, or USDC via the PAYMENT-SIGNATURE header / two-step flow).
Request body
| domain* | string | Domain to give the agent. Bought if not already owned. |
| slug | string | Mailbox local part (hi@domain). Default 'hi'. |
| name | string | Display name for outbound email. |
| webhook_url | string (uri) | Optional HTTPS URL to receive inbound email + domain events. |
| webhook_headers | object | Optional delivery authentication. Values are encrypted at rest and never returned; responses expose header_names only. |
| webhook_events | string[] | Webhook event types. Defaults to email.received/delivered/bounced. |
| payment_method | string(card | usdc | balance) | How to pay for the domain. |
| payment_tx | string | USDC tx hash (two-step flow). |
| payment_chain | string(base | ethereum) | |
| years | integer | |
| max_price | number | Ceiling in USD for the domain charge. If the real price exceeds it, the request fails with 400 PRICE_ABOVE_MAX and nothing is charged. The error's `price` field is the total to accept; confirm it with the user before retrying with that max_price. |
Response 200
| domain | string | |
| domain_status | string | |
| mailbox | object | null | |
| webhook | object | null | |
| warnings | string[] | |
| hint | string | |
| next_steps | string[] |
Example
curl -X POST https://domani.run/api/agents/provision \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"domain":"myagent.run","slug":"hi","webhook_url":"https://my-agent.example.com/inbox","webhook_headers":{"Authorization":"Bearer sender-key"}}'/api/domains/buyPurchase one or more domains
Purchases one or more domains and registers them immediately. Send {"domain": "..."} for single or {"domains": ["...", "..."]} for bulk (max 10). If the domain is listed for sale on the Domani marketplace, this endpoint automatically routes to a marketplace purchase instead of a fresh registration. The response includes years: 0 and marketplace_purchase: true to distinguish it from a registration. Marketplace purchases are single-domain only. The buyer pays the full listed price; the seller receives the price minus a platform commission (default 10%). Bulk purchases process each domain independently - failures don't block other purchases. The response includes results (succeeded), errors (failed), and summary with counts. Accepts three payment methods: 1. Card (default): Requires a card on file via POST /api/billing/setup. Charged per domain. 2. USDC (two-step): First call without payment to get a 402 with payment_options.crypto (wallet address, amount, chains). Send USDC on-chain, then retry with payment_tx and payment_chain in the request body. 3. x402 USDC (atomic): Include a PAYMENT-SIGNATURE header with a signed USDC payment on Base. Bulk purchases require a card on file (USDC not supported for multi-domain). If no payment is provided for single domain, returns HTTP 402 with payment instructions.
Request body
| domain | string | Single domain to purchase (e.g. mysite.com) |
| domains | string[] | Array of domains to purchase (max 10, requires card on file) |
| payment_method | string(card | usdc | balance) | Payment method: 'card' (default), 'usdc' (x402 - first call returns 402 with payment instructions), or 'balance' (spend marketplace credit). |
| payment_tx | string | Transaction hash of a USDC payment already sent on-chain. Required on the second call of the two-step USDC flow. |
| payment_chain | string(base | ethereum) | Chain the USDC payment was sent on (e.g. 'base' or 'ethereum'). |
| years | integer | Number of years to register (1-10, default 1). Price is multiplied by years. |
| max_price | number | Ceiling in USD for the total charge. If the real price exceeds it, the request fails with 400 PRICE_ABOVE_MAX and nothing is charged - a safety belt for autonomous agents. Without max_price the ceiling is the price search listed: when only a registrar billing more can take the name, the buy fails the same way. The error's `price` is the total to accept, with `listed_price` and `renewal_price` beside it; confirm them with the user before retrying with that max_price. |
| ref | string | Referral code (optional) - the referrer earns a commission |
Example
curl -X POST https://domani.run/api/domains/buy \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"domain": "mysite.com"}'/api/domains/{domain}Get detailed information about a domain you own
Returns comprehensive information about a domain: status, purchase date, expiry, payment method, security lock, WHOIS privacy, auto-renew status, and registration dates. The registrar object may be null if extended data is unavailable.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| status | string | |
| auto_renew | boolean | Whether auto-renew is enabled |
| purchased_at | string (date-time) | |
| expires_at | string (date-time) | |
| days_until_expiry | integer | Days until domain expiry |
| renewal_price | number | null | What one year of renewal costs in USD at the registrar that holds the domain; renewing N years costs N times this. Null when domani cannot renew the domain or its price is unknown. |
| payment_method | string | How the domain was paid for |
| payment_tx | string | null | On-chain tx hash (USDC payments) |
| registrant | string | Who the registry lists as the domain's registrant. `customer`: your own contact details (from set_contact). `domani`: Domani's default contact, holding the domain on your behalf until you add yours with set_contact, which makes you the registrant. `unknown`: not checked yet. WHOIS privacy hides the details either way. |
| transfer_out | object | Moving the domain to another registrar. `mode` `api`: get_auth_code returns the code. `manual`: the registrar has no API for it, so Domani's team gets it by hand within 1 business day. null: not available for this domain. |
| registrar_status | string | Whether live registrar data was refreshed or is managed externally |
| registrar_checked_at | string (date-time) | When the registrar refresh was attempted |
| registrar | object | null | Domain registration details (null if unavailable) |
Example response
{
"domain": "mysite.com",
"status": "active",
"auto_renew": true,
"purchased_at": "2025-06-01T12:00:00.000Z",
"expires_at": "2026-06-01T12:00:00.000Z",
"days_until_expiry": 365,
"payment_method": "card",
"registrant": "customer",
"transfer_out": {
"mode": "api",
"locked_until": null,
"request": null
},
"registrar_status": "connected",
"registrar_checked_at": "2025-06-01T12:00:00.000Z",
"registrar": {
"security_lock": true,
"whois_privacy": true,
"auto_renew": true,
"create_date": "2025-06-01T12:00:00Z",
"expire_date": "2026-06-01T12:00:00Z"
}
}Example
curl https://domani.run/api/domains/mysite.com \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/settingsUpdate domain settings
Update settings for a domain you own. Supports auto_renew, whois_privacy, and security_lock. At least one field must be provided.
Parameters
| domain* | string | Domain name you own |
Request body
| auto_renew | boolean | Whether to enable auto-renew for this domain |
| whois_privacy | boolean | Whether to enable WHOIS privacy protection |
| security_lock | boolean | Whether to lock the domain against transfers |
Response 200
| domain | string | |
| auto_renew | boolean | |
| whois_privacy | boolean | |
| security_lock | boolean | |
| hint | string |
Example
curl -X PUT https://domani.run/api/domains/mysite.com/settings \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"auto_renew": false}'/api/domains/{domain}/parkingUpdate parking settings
Update parking and listing settings for a domain you own. Enable/disable the parking page and set a 'For Sale' listing price. When parking is enabled, visitors to the domain see a branded page. When a listing price is set, visitors see a 'For Sale' page with a contact form.
Parameters
| domain* | string | Domain name |
Request body
| enabled | boolean | Whether to enable the parking page |
| listing_price | number | null | Sale price in USD, or null to remove listing |
Response 200
| domain | string | |
| parking_enabled | boolean | |
| listing_price | number | null | |
| hint | string |
/api/domains/{domain}/redirectForward a domain to a URL
Forward a domain you own to another URL (e.g. brand.com -> https://brand.dev) - useful for multi-TLD brands. The domain must point at domani's nameservers/parking IP for the redirect to serve. Permanent (308) by default or temporary (307). Requires domains:write scope.
Parameters
| domain* | string | Domain name |
Request body
| url* | string | Target URL (or bare domain) to forward to, e.g. https://brand.dev |
| permanent | boolean | true = permanent (308, default), false = temporary (307) |
Response 200
| domain | string | |
| redirect_url | string | null | |
| permanent | boolean |
/api/domains/{domain}/redirectStop forwarding a domain
Remove a domain's redirect so it reverts to the parking page. Requires domains:write scope.
Parameters
| domain* | string | Domain name |
Response 200
| domain | string | |
| redirect_url | string | null |
/api/domains/{domain}/for-saleList a domain for sale
List a domain you own for sale on the Domani marketplace. Buyers can purchase it directly via the API. The domain must be active and not already listed. Fees: Domani charges a platform commission (default 10%) on successful sales. The seller receives the listed price minus the commission. The buyer pays the full listed price. Requires domains:write scope.
Parameters
| domain* | string | Domain name to list for sale |
Request body
| price* | number | Sale price in USD |
| description | string | Optional listing description |
Response 200
| listing | object | |
| hint | string |
Example response
{
"listing": {
"id": "lst_abc123",
"domain": "example.com",
"price": 5000,
"currency": "USD",
"description": null,
"status": "active",
"created_at": "2026-03-14T12:00:00.000Z"
},
"hint": "example.com listed for sale at $5,000. Buyers can purchase it on the marketplace."
}/api/domains/{domain}/for-saleUpdate a domain listing
Update the price and/or description of an active marketplace listing. At least one field must be provided. Requires domains:write scope.
Parameters
| domain* | string | Domain name with active listing |
Request body
| price | number | New sale price in USD |
| description | string | null | New description (null to clear) |
Response 200
| listing | object | |
| hint | string |
Example response
{
"listing": {
"id": "lst_abc123",
"domain": "example.com",
"price": 7000,
"currency": "USD",
"description": "Premium domain",
"status": "active",
"created_at": "2026-03-14T12:00:00.000Z"
},
"hint": "Listing for example.com has been updated. New price: $7,000."
}/api/domains/{domain}/for-saleCancel a domain listing
Remove an active for-sale listing for a domain you own. The domain remains in your account but is no longer available for purchase on the marketplace. Requires domains:write scope.
Parameters
| domain* | string | Domain name to unlist |
Response 200
| domain | string | |
| status | string | |
| hint | string |
Example response
{
"domain": "example.com",
"status": "cancelled",
"hint": "Listing for example.com has been cancelled."
}/api/domains/{domain}/analyticsGet parking analytics
Get visitor analytics for a parked domain - page views, inquiries, conversion rate, 30-day daily breakdown, and recent inquiries.
Parameters
| domain* | string | Domain name |
Response 200
| domain | string | |
| views_7d | integer | Total page views in the last 7 days |
| views_30d | integer | Total page views in the last 30 days |
| inquiries_30d | integer | Total inquiries in the last 30 days |
| conversion_rate | number | Inquiry-to-view ratio as percentage |
| daily_views | object[] | Daily view counts for the last 30 days |
| recent_inquiries | object[] | 5 most recent inquiries |
| hint | string |
Example response
{
"domain": "example.com",
"views_7d": 42,
"views_30d": 156,
"inquiries_30d": 3,
"conversion_rate": 1.92,
"daily_views": [
{
"date": "2026-02-25",
"views": 7
}
],
"recent_inquiries": [
{
"email": "buyer@example.com",
"offer": 500,
"date": "2026-02-25T10:00:00Z"
}
],
"hint": "example.com received 156 views and 3 inquiries in the last 30 days (1.92% conversion)."
}/api/domains/{domain}/auth-codeGet EPP auth code
Get the EPP/auth code needed to transfer a domain to another registrar. Automatically unlocks the domain if it's locked. Whoever holds the auth code can move the domain, and its mail with it: reading it needs the email:auth_secrets scope and a token not confined to mailboxes (403 INSUFFICIENT_SCOPE or 403 MAILBOX_CONFINED otherwise), and every read is recorded in the audit log. Some registrars have no API for the code: the first call then answers 202 with status: "requested" while Domani's team unlocks the domain and gets the code by hand (within 1 business day, expected_by); the user gets an email when it is ready, and a later call answers 200 with status: "ready" and the code. Within 60 days of a registration or transfer-in such a domain answers 409 TRANSFER_LOCKED with available_at.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| status | string | `ready`: the code is in auth_code. `requested`: Domani's team is getting it from the registrar; the user gets an email when it is ready. |
| auth_code | string | null | EPP auth code to give to the new registrar. null while requested. |
| was_unlocked | boolean | Whether the domain was unlocked to allow the transfer |
| requested_at | string (date-time) | When the code was asked for (registrars without an API for it) |
| expected_by | string (date-time) | When Domani's team will have the code at the latest (status requested) |
| hint | string | |
| next_steps | string[] |
/api/domains/{domain}/transfer-awayGet outbound transfer status
Check the status of an outbound domain transfer. Use after getting an auth code and initiating the transfer at the new registrar. Not available on a domain whose transfer_out.mode is manual.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| status | string | |
| gaining_registrar | string | |
| request_date | string (date-time) | |
| hint | string |
/api/domains/{domain}/transfer-statusCheck inbound transfer status
Check the status of an inbound domain transfer. Returns detailed status with actionable hints. Use after initiating a transfer with POST /api/domains/transfer.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| status | string | |
| timestamp | string | Timestamp of last status update |
| hint | string | Actionable guidance based on the current status |
| continuity | object | Captured delegation and post-transfer verification for safe transfers. |
/api/domains/importImport an external domain
Import a domain you own at another registrar (GoDaddy, Namecheap, Cloudflare, etc.) to manage through domani.run. Free, no transfer needed. Returns a TXT record for DNS-based ownership verification.
Request body
| domain* | string | Domain to import |
Response 200
| domain | string | |
| status | string | |
| token | string | |
| txt_record | object | |
| hint | string |
/api/domains/import/verifyVerify and complete domain import
Verify DNS TXT record and complete domain import. Call after adding the TXT record returned by POST /api/domains/import. Uses Cloudflare DNS-over-HTTPS to check propagation.
Request body
| domain* | string | Domain to verify |
Response 201
| domain | string | |
| status | string | |
| expires | string (date-time) | |
| registrar | string | |
| payment_method | string | |
| hint | string |
/api/domains/adoption-planPlan how to adopt an existing domain
Read-only inspection of an existing domain before connecting or transferring it. Returns the current registrar, authoritative nameservers, DNS provider, DNSSEC state, account state, transfer price, safety invariants, and exact next API actions. Call this before requesting an EPP code. Connecting leaves registrar and DNS unchanged. Transferring preserves nameservers and does not migrate DNS.
Parameters
| domain* | string | Existing domain to inspect, e.g. mysite.com |
Response 200
| version | integer | |
| domain | string | |
| registered | boolean | |
| account_state | string | |
| recommended_action | string | |
| current | object | |
| options | object | |
| invariants | string[] | |
| warnings | string[] |
/api/domains/transfer-checkPre-check transfer eligibility
Check whether a domain is eligible for transfer before initiating. Returns transfer price, eligibility status, and any blockers (unsupported TLD, ICANN waiting period, domain locked). Call this before POST /api/domains/transfer.
Parameters
| domain* | string | Domain to check, e.g. mysite.com |
Response 200
| domain | string | |
| tld | string | |
| eligible | boolean | |
| price | number | null | Transfer price in USD (null if TLD unsupported) |
| currency | string | |
| renewal_price | number | Eligible only: what each year after the transfer costs, in USD, at the registrar it moves to. Absent when unknown. |
| reason | string | Human-readable reason if not eligible |
| code | string | |
| eligible_at | string (date) | ISO date when domain becomes eligible (ICANN waiting period) |
| hint | string |
/api/domains/transfer-watchWatch a domain for transfer eligibility
Watch a domain and get notified (email + transfer.eligible webhook) when it becomes eligible for transfer. Uses RDAP to check EPP status codes and ICANN 60-day lock windows. If already eligible, returns immediately without creating a watch.
Request body
| domain* | string | Domain to watch, e.g. mysite.com |
Response 200
| domain | string | |
| eligible | boolean | Whether the domain is currently eligible for transfer |
| eligible_at | string (date) | ISO date when domain becomes eligible (if not yet eligible) |
| hint | string | Actionable guidance |
| watching | boolean | Whether a notification has been set up |
/api/domains/transferTransfer domain from another provider
Initiate a domain transfer-in after planDomainAdoption and explicit price confirmation. Requires the authorization/EPP code from the current provider. The existing authoritative nameservers are preserved and verified. DNS migration is never implicit. Transfer typically takes 1-5 days to complete. Payment is charged upfront via card or USDC.
Request body
| domain* | string | Domain to transfer, e.g. mysite.com |
| auth_code* | string | Authorization/EPP code from the current provider |
| payment_method | string(card | usdc | balance) | Payment method: 'card' (default), 'usdc', or 'balance' (marketplace credit). |
| payment_tx | string | Transaction hash of a USDC payment already sent on-chain. |
| payment_chain | string(base | ethereum) | Chain the USDC payment was sent on. |
| extra_subdomains | string[] | Additional subdomains to include in the pre-transfer DNS snapshot. We auto-discover subdomains via CT logs, SPF, MX/DKIM inference, and a common wordlist - use this for any custom subdomains we might miss. |
| max_price | number | Ceiling in USD for the transfer charge. If the real price exceeds it, the request fails with 400 PRICE_ABOVE_MAX and nothing is charged. Without max_price, the ceiling is the listed transfer price: when only a dearer registrar can take it, the transfer fails the same way. The error's `price` field is the total to accept; confirm it with the user before retrying with that max_price. |
Response 202
| domain | string | |
| status | string | |
| expires | string (date-time) | |
| price | number | |
| currency | string | |
| payment_method | string | |
| payment_tx | string | |
| auto_renew | boolean | |
| hint | string | |
| dns_snapshot | object | Recovery backup summary captured before transfer. It is not applied unless an explicit recovery operation is needed. |
| nameservers_preserved | boolean | |
| dns_migration | string | |
| continuity | object |
Example
curl -X POST https://domani.run/api/domains/transfer \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"domain": "mysite.com", "auth_code": "EPP-AUTH-CODE"}'/api/domains/{domain}/renewRenew a domain
Renew a domain you own for additional years (1-10). Payment is charged upfront. The new expiry date is returned in the response. When the domain's registrar refuses the renewal (renewed in the last 30 days, or a term it does not sell), the request fails with 400 RENEWAL_NOT_ELIGIBLE before any charge. The price is renewal_price from GET /api/domains/{domain} times years: send that total as max_price so a price change fails before any charge.
Parameters
| domain* | string | Domain name you own |
Request body
| years | integer | Number of years to renew (default 1) |
| payment_method | string(card | usdc | balance) | Payment method: 'card' (default), 'usdc', or 'balance' (marketplace credit). |
| payment_tx | string | Transaction hash of a USDC payment already sent on-chain. |
| payment_chain | string(base | ethereum) | Chain the USDC payment was sent on. |
| max_price | number | Ceiling in USD for the renewal charge. If the real price exceeds it, the request fails with 400 PRICE_ABOVE_MAX and nothing is charged. The error's `price` field is the total to accept; confirm it with the user before retrying with that max_price. |
Response 200
| domain | string | |
| renewed_years | integer | |
| new_expiry | string (date-time) | |
| price | number | |
| currency | string | |
| payment_method | string | |
| payment_tx | string |
Example
curl -X POST https://domani.run/api/domains/mysite.com/renew \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"years": 1}'/api/domains/{domain}/buy-aftermarketBuy an aftermarket domain
Buy a taken domain that's listed for sale on an aftermarket (Afternic / Sedo) at its buy-now price, through Domani. The listing price is re-checked live before charging; pass max_price to cap what you'll pay. Make-offer-only listings can't be bought here - use the broker (POST /api/broker) to negotiate a price instead. Payment works like a registration: card, USDC on Base (x402), or marketplace balance. Requires the domains:transfer scope. On success the domain lands in your account (status: active, or provisioning while a staged transfer finalizes).
Parameters
| domain* | string | The aftermarket-listed domain to buy |
Request body
| max_price | number | Hard price ceiling in USD. Defaults to the listing's buy-now price; the purchase is rejected if the live price exceeds this. |
| payment_method | string(card | usdc | balance) | Payment method: 'card' (default), 'usdc', or 'balance' (marketplace credit). |
| payment_tx | string | Transaction hash of a USDC payment already sent on-chain. |
| payment_chain | string(base | ethereum) | Chain the USDC payment was sent on. |
Response 201
| domain | string | |
| status | string | 'active' when the purchase completed, or 'provisioning' while a staged transfer finalizes. |
| price | number | |
| currency | string | |
| payment_method | string | |
| payment_tx | string | |
| marketplace | string | Aftermarket the domain was bought on (e.g. 'Afternic', 'Sedo'). |
| hint | string | |
| next_steps | string[] |
Example
curl -X POST https://domani.run/api/domains/coolname.com/buy-aftermarket \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{}'DNS
/api/domains/{domain}/dnsGet DNS records for a domain you own
Returns all DNS records configured for a domain you own (A, AAAA, CNAME, MX, TXT, NS, ...), each with a stable id, plus a zone_version token (also sent as the ETag header). Pass zone_version back on PUT to detect concurrent zone changes.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| records | DnsRecord[] | |
| zone_version | string | Optimistic-concurrency token for the whole zone. Pass it back on PUT (body zone_version or If-Match header) to fail with 409 DNS_VERSION_CONFLICT instead of clobbering a concurrent change. |
Example response
{
"domain": "mysite.com",
"records": [
{
"id": "rec_1a2b3c4d5e6f",
"type": "A",
"name": "@",
"value": "76.76.21.21",
"ttl": 3600
},
{
"id": "rec_2b3c4d5e6f7a",
"type": "CNAME",
"name": "www",
"value": "mysite.com",
"ttl": 3600
},
{
"id": "rec_3c4d5e6f7a8b",
"type": "MX",
"name": "@",
"value": "mail.example.com",
"ttl": 3600,
"priority": 10
}
],
"zone_version": "zv_9f8e7d6c5b4a3f2e"
}Example
curl https://domani.run/api/domains/mysite.com/dns \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/dnsSet DNS records for a domain you own
Upserts DNS records at the rrset level: for each (type, name) present in the payload, existing records at that (type, name) are replaced by the ones you send; rrsets NOT mentioned in the payload are preserved, and NS records are never touched. An automatic server-side backup of the zone is taken before every write. Concurrency: pass the zone_version from a prior GET (body field or If-Match header). If the zone changed since, the write fails with 409 DNS_VERSION_CONFLICT and returns the current records + version so you can re-merge and retry. Mail routing: a change to where the domain's mail is delivered (its MX or NS records, the address records an MX host inside the zone resolves through, or those of a host that receives Domani mail and publishes no MX) needs the email:auth_secrets scope, since whoever receives the mail receives its sign-in codes. Without it the request answers 403 INSUFFICIENT_SCOPE before anything changes, and a token confined to mailboxes answers 403 MAILBOX_CONFINED even with it. Other records need only domains:write. Every routing change is recorded in the audit log.
Parameters
| domain* | string | Domain name you own |
Request body
| records* | DnsRecord[] | |
| zone_version | string | The zone_version from a prior GET. On mismatch the write is rejected with 409 DNS_VERSION_CONFLICT (nothing written). Recommended whenever another actor may edit the same zone. |
Response 200
| success | boolean | |
| domain | string | |
| records | DnsRecord[] |
Example response
{
"success": true,
"domain": "mysite.com",
"records": [
{
"type": "A",
"name": "@",
"value": "76.76.21.21",
"ttl": 3600
},
{
"type": "CNAME",
"name": "www",
"value": "cname.vercel-dns.com",
"ttl": 3600
}
]
}Example
curl -X PUT https://domani.run/api/domains/mysite.com/dns \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"records": [
{"type": "A", "name": "@", "value": "76.76.21.21", "ttl": 3600},
{"type": "CNAME", "name": "www", "value": "cname.vercel-dns.com", "ttl": 3600}
]}'/api/domains/{domain}/dnssecList DNSSEC DS records for a domain you own
Returns the DNSSEC delegation-signer (DS) records published at the registry for a domain you own, and whether DNSSEC is enabled. Pairs with TLSA records (set via the DNS endpoint) for DANE.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| enabled | boolean | True when at least one DS record is published |
| records | DsRecord[] |
Example response
{
"domain": "mysite.com",
"enabled": true,
"records": [
{
"keyTag": "12345",
"algorithm": "13",
"digestType": "2",
"digest": "A1B2C3..."
}
]
}Example
curl https://domani.run/api/domains/mysite.com/dnssec \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/dnssecPublish a DNSSEC DS record (enable DNSSEC)
Publishes a delegation-signer (DS) record at the registry, enabling DNSSEC for the domain. Get the DS values from your DNS/zone provider after signing the zone.
Parameters
| domain* | string | Domain name you own |
Response 200
| success | boolean | |
| domain | string | |
| enabled | boolean | |
| records | DsRecord[] |
Example
curl -X POST https://domani.run/api/domains/mysite.com/dnssec \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"keyTag": "12345", "algorithm": "13", "digestType": "2", "digest": "A1B2C3..."}'/api/domains/{domain}/dnssecRemove a DNSSEC DS record (disable DNSSEC)
Removes a delegation-signer (DS) record at the registry by its key tag. Removing all DS records disables DNSSEC for the domain.
Parameters
| domain* | string | Domain name you own |
| keyTag* | string | Key tag of the DS record to remove |
Response 200
| success | boolean | |
| domain | string | |
| enabled | boolean | |
| records | DsRecord[] |
Example
curl -X DELETE "https://domani.run/api/domains/mysite.com/dnssec?keyTag=12345" \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/dns/snapshotCapture a DNS snapshot
Captures all DNS records for a domain via public DNS lookups (Cloudflare DoH). Discovers subdomains from Certificate Transparency logs, SPF records, and common names. The snapshot is also stored server-side as a backup.
Parameters
| domain* | string | Domain name you own |
Request body
| extra_subdomains | string[] | Additional subdomains to include in the snapshot |
Response 200
| domain | string | |
| records | DnsRecord[] | |
| subdomains | string[] | |
| sources | string[] | |
| capturedAt | string (date-time) |
Example response
{
"domain": "mysite.com",
"records": [
{
"type": "A",
"name": "@",
"value": "76.76.21.21",
"ttl": 3600
},
{
"type": "CNAME",
"name": "www",
"value": "mysite.com",
"ttl": 3600
},
{
"type": "MX",
"name": "@",
"value": "aspmx.l.google.com",
"ttl": 3600,
"priority": 1
}
],
"subdomains": [
"www",
"mail",
"api"
],
"sources": [
"ct",
"wordlist"
],
"capturedAt": "2026-03-06T12:00:00.000Z"
}Example
curl -X POST https://domani.run/api/domains/mysite.com/dns/snapshot \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/dns/restoreRestore DNS from a snapshot
Applies DNS records from a snapshot to the domain's current registrar. Uses a diff algorithm to avoid duplicating records that already exist. If no snapshot is provided in the body, restores from the server-side backup (created by snapshot or transfer). Mail routing: a change to where the domain's mail is delivered (its MX or NS records, the address records an MX host inside the zone resolves through, or those of a host that receives Domani mail and publishes no MX) needs the email:auth_secrets scope, since whoever receives the mail receives its sign-in codes. Without it the request answers 403 INSUFFICIENT_SCOPE before anything changes, and a token confined to mailboxes answers 403 MAILBOX_CONFINED even with it. Other records need only domains:write. Every routing change is recorded in the audit log.
Parameters
| domain* | string | Domain name you own |
Request body
| snapshot | object | A DNS snapshot object (as returned by the snapshot endpoint). If omitted, the stored server-side backup is used. |
| dry_run | boolean | Preview only: returns what the restore would create (would_apply) and skip, without writing anything. |
Response 200
| domain | string | |
| applied | integer | Number of records created |
| skipped | integer | Number of records already present |
| recordCount | integer | Total records in the snapshot |
| errors | string[] | |
| dry_run | boolean | True when this was a preview - nothing was written |
| would_apply | DnsRecord[] | Dry run only: the records the restore would create |
Example response
{
"domain": "mysite.com",
"applied": 5,
"skipped": 2,
"recordCount": 7,
"errors": []
}Example
curl -X POST https://domani.run/api/domains/mysite.com/dns/restore \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/dns/cloneClone another domain's DNS onto this one
Copies the DNS setup of a source domain you own onto this domain - useful for multi-TLD brands (brand.com → brand.dev) that share the same A/CNAME/MX. Merges by default (source records win on any type+name collision, this domain's other records are kept); set replace: true for an exact mirror. Mail routing: a change to where the domain's mail is delivered (its MX or NS records, the address records an MX host inside the zone resolves through, or those of a host that receives Domani mail and publishes no MX) needs the email:auth_secrets scope, since whoever receives the mail receives its sign-in codes. Without it the request answers 403 INSUFFICIENT_SCOPE before anything changes, and a token confined to mailboxes answers 403 MAILBOX_CONFINED even with it. Other records need only domains:write. Every routing change is recorded in the audit log.
Parameters
| domain* | string | Target domain (DNS is written here) |
Request body
| from* | string | Source domain to copy DNS from (must be yours) |
| replace | boolean | Exact mirror - drop target records the source doesn't have. Default: merge |
Response 200
| success | boolean | |
| from_domain | string | |
| to_domain | string | |
| records | DnsRecord[] |
Example
curl -X POST https://domani.run/api/domains/brand.dev/dns/clone \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"from": "brand.com"}'/api/domains/{domain}/nameserversGet nameservers for a domain
Returns the authoritative nameservers configured for a domain you own. If empty, DNS operations (parking, email, connect) will fail until nameservers are assigned.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| nameservers | string[] | |
| default_nameservers | string[] | The nameservers the domain's zone is served from: the zone host when Domani hosts the zone there, otherwise the registrar's defaults (null if unknown) |
| dns_provider | string | Who answers for the domain's zone: the registrar's own DNS, a zone Domani hosts on Cloudflare, or an external provider for an imported domain |
| hint | string |
Example response
{
"domain": "mysite.com",
"nameservers": [
"ns1.systemdns.com",
"ns2.systemdns.com",
"ns3.systemdns.com"
],
"default_nameservers": [
"ns1.systemdns.com",
"ns2.systemdns.com",
"ns3.systemdns.com"
],
"dns_provider": "registrar",
"hint": "3 nameservers configured for mysite.com."
}Example
curl https://domani.run/api/domains/mysite.com/nameservers \ -H "Authorization: Bearer domani_sk_xxx"
/api/domains/{domain}/nameserversSet nameservers for a domain
Replace the nameservers for a domain you own. Requires 2-13 valid hostnames. Common values: OpenSRS DNS (ns1.systemdns.com, ns2.systemdns.com, ns3.systemdns.com). Nameservers decide where all of the domain's mail goes, sign-in codes included: changing them needs the email:auth_secrets scope and a token not confined to mailboxes (403 INSUFFICIENT_SCOPE or 403 MAILBOX_CONFINED otherwise), and every change is recorded in the audit log.
Parameters
| domain* | string | Domain name you own |
Request body
| nameservers* | string[] | Nameserver hostnames (2-13 required) |
Response 200
| success | boolean | |
| domain | string | |
| nameservers | string[] | |
| hint | string |
Example response
{
"success": true,
"domain": "mysite.com",
"nameservers": [
"ns1.systemdns.com",
"ns2.systemdns.com",
"ns3.systemdns.com"
],
"hint": "Nameservers for mysite.com updated to ns1.systemdns.com, ns2.systemdns.com, ns3.systemdns.com. Changes may take up to 48 hours to propagate."
}Example
curl -X PUT https://domani.run/api/domains/mysite.com/nameservers \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"nameservers": ["ns1.systemdns.com", "ns2.systemdns.com", "ns3.systemdns.com"]}'Connect
/api/domains/{domain}/connectConnect a domain to a hosting or email provider
Connects a domain to a hosting or email provider by auto-detecting the provider from a target URL or accepting an explicit provider name. For domains managed through the platform, DNS records are set automatically (status: dns_set). For imported domains (external registrar), returns the records as instructions to add manually at your registrar (status: manual_setup_required, records have status pending). Supported hosting providers: vercel, netlify, cloudflare-pages, github-pages, railway, fly. Supported email providers: google-workspace, fastmail, proton. Use GET /api/domains/{domain}/connect to list all available providers and methods. Mail routing: a change to where the domain's mail is delivered (its MX or NS records, the address records an MX host inside the zone resolves through, or those of a host that receives Domani mail and publishes no MX) needs the email:auth_secrets scope, since whoever receives the mail receives its sign-in codes. Without it the request answers 403 INSUFFICIENT_SCOPE before anything changes, and a token confined to mailboxes answers 403 MAILBOX_CONFINED even with it. Other records need only domains:write. Every routing change is recorded in the audit log.
Parameters
| domain* | string | Domain name you own |
Request body
| target | string | Target URL for auto-detection (e.g. my-app.vercel.app) |
| provider | string | Explicit provider name (e.g. vercel, google-workspace) |
| method | string | Connection method if provider has multiple (e.g. cname-only) |
| dry_run | boolean | Preview only: returns the diff (create/replace/already_set/keep) without writing anything. |
| confirm_replace_mx | boolean | Required when the connect would replace existing MX records pointing at another provider (409 MX_REPLACEMENT_REQUIRES_CONFIRMATION otherwise) - it reroutes the domain's email. |
Response 200
| domain | string | |
| provider | string | |
| category | string | |
| method | string | |
| records | DnsRecord[] | |
| docs | string (uri) | |
| status | string | dns_set = records written automatically. manual_setup_required = imported domain, add records at your registrar. dry_run = preview only, nothing written. |
| hint | string | |
| next_steps | string[] | Provider-specific actions to complete after DNS records are set |
| diff | object | Dry run only: what the write would do - create, replace (existing records that would be removed), already_set, keep (untouched rrsets). |
Example
curl -X POST https://domani.run/api/domains/mysite.com/connect \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"target": "my-app.vercel.app"}'/api/domains/{domain}/connectList available providers for connecting a domain
Returns all supported hosting and email providers with their connection methods. Use this to discover what providers are available before calling POST.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| providers | object |
/api/domains/{domain}/verifyVerify that a provider connection is working
Runs the provider's verification check to confirm DNS records have propagated and the connection is active. Use after POST /api/domains/{domain}/connect to verify.
Parameters
| domain* | string | Domain name you own |
Request body
| target | string | Target for auto-detection |
| provider | string | Provider name |
| method | string | Method name |
Response 200
| domain | string | |
| provider | string | |
| method | string | |
| verified | boolean | |
| message | string | |
| hint | string | |
| next_steps | string[] | Provider-specific troubleshooting steps (only when verified is false) |
Example
curl -X POST https://domani.run/api/domains/mysite.com/verify \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"target": "my-app.vercel.app"}'Status
/api/domains/{domain}/statusCheck domain health: DNS, SSL, email, expiry
Checks the health of a domain via DNS-over-HTTPS: A/AAAA/CNAME/MX/TXT/NS records, SSL certificate status, email configuration, and expiry date. Supports SSE streaming via Accept: text/event-stream - closing the connection cancels in-flight DNS/SSL checks server-side. Use this after connecting to verify DNS propagation.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| status | string | |
| expires | string (date-time) | |
| days_until_expiry | integer | |
| dns | object | |
| ssl | object | |
| object |
Example response
{
"domain": "mysite.com",
"status": "active",
"expires": "2027-02-18T00:00:00.000Z",
"days_until_expiry": 365,
"dns": {
"propagated": true,
"a": [
"76.76.21.21"
],
"aaaa": [],
"cname": [],
"mx": [],
"txt": [],
"ns": [
"ns1.porkbun.com",
"ns2.porkbun.com"
],
"registrar_records": [
{
"type": "A",
"name": "@",
"value": "76.76.21.21",
"ttl": 3600
}
]
},
"ssl": {
"active": true
},
"email": {
"configured": false,
"mx": []
}
}Example
curl https://domani.run/api/domains/mysite.com/status \ -H "Authorization: Bearer domani_sk_xxx"
Verify Service
/api/domains/{domain}/verify-serviceAdd DNS records to verify domain ownership for a third-party service
Adds the correct DNS records (TXT, CNAME) to prove domain ownership to a third-party service. Supports Stripe, Google Search Console, AWS SES, Postmark, Resend, Facebook, HubSpot, and Microsoft 365. Unknown service names fall back to a generic TXT record. Records are merged with existing DNS - existing records are preserved. Mail routing: a change to where the domain's mail is delivered (its MX or NS records, the address records an MX host inside the zone resolves through, or those of a host that receives Domani mail and publishes no MX) needs the email:auth_secrets scope, since whoever receives the mail receives its sign-in codes. Without it the request answers 403 INSUFFICIENT_SCOPE before anything changes, and a token confined to mailboxes answers 403 MAILBOX_CONFINED even with it. Other records need only domains:write. Every routing change is recorded in the audit log.
Parameters
| domain* | string | Domain name you own |
Request body
| service* | string | Service name (e.g. stripe, google-search-console, aws-ses, postmark, resend, facebook, hubspot, microsoft-365) |
| token* | string | Verification token provided by the service |
Response 200
| domain | string | |
| service | string | |
| service_display_name | string | |
| known_service | boolean | |
| records_added | DnsRecord[] | |
| total_records | integer | |
| docs | string (uri) | null | |
| hint | string |
Example response
{
"domain": "mysite.com",
"service": "stripe",
"service_display_name": "Stripe",
"known_service": true,
"records_added": [
{
"type": "TXT",
"name": "@",
"value": "stripe-verification=abc123def456",
"ttl": 3600
}
],
"total_records": 3,
"docs": "https://docs.stripe.com/identity/verification-checks/selfie#domain-verification",
"hint": "Stripe verification records added. Return to Stripe to complete verification."
}Example
curl -X POST https://domani.run/api/domains/mysite.com/verify-service \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"service": "stripe", "token": "abc123def456"}'/api/domains/{domain}/verify-serviceList supported services for domain verification
Returns all known services with their names, descriptions, and documentation URLs. Unknown services fall back to a generic TXT record.
Parameters
| domain* | string | Domain name you own |
Response 200
| domain | string | |
| services | object[] | |
| hint | string |
Discovery
/api/domains/{domain}/profilePublicGet a public domain profile
Returns the latest privacy-safe, evidence-backed public dossier for a domain. This endpoint never returns redacted observations or raw registration contact details.
Parameters
| domain* | string |
Response 200
| profile | PublicDomainProfile |
/api/domains/{domain}/profilePublicResearch or refresh a public domain profile
Runs a bounded analysis using public registry, DNS, live-site, structured-data, and archive sources, then stores the privacy-safe profile for the canonical public page. Instant depth is used for a fast first render; deep is the default and adds historical research.
Parameters
| domain* | string | |
| depth | string(instant | deep) | Research depth. Defaults to deep. |
Response 201
| profile | PublicDomainProfile |
/api/domains/whoisPublicLook up domain registration data via RDAP
Queries the RDAP (Registration Data Access Protocol) network for domain registration data. Returns registrar, creation/expiry/update dates, days until expiry, status codes, nameservers, DNSSEC status, and contact information (registrant, admin, tech, billing, abuse). Works for any domain - no ownership required. RDAP is the modern, JSON-native replacement for WHOIS. Falls back to WHOIS (port 43) for TLDs without RDAP, and supplements RDAP with WHOIS referral data when contacts are missing (common post-GDPR). Contact information may be partially or fully redacted due to GDPR or privacy protection services. Public endpoint - no authentication required. Authenticated requests get higher rate limits (30/min vs 10/min).
Parameters
| q* | string | Domain to look up (e.g. example.com) |
Response 200
| domain | string | |
| registered | boolean | Whether the domain is currently registered |
| registrar | string | null | Current registrar name |
| registrar_url | string | null | Registrar website URL |
| registrar_iana_id | string | null | Registrar IANA ID (from RDAP publicIds) |
| created | string | null | Registration date (YYYY-MM-DD) |
| expires | string | null | Expiration date (YYYY-MM-DD) |
| updated | string | null | Last updated date (YYYY-MM-DD) |
| days_until_expiry | integer | null | Days until expiry (null if not registered) |
| status | string[] | EPP status codes |
| nameservers | string[] | Authoritative nameservers |
| dnssec | boolean | Whether DNSSEC is enabled |
| redacted | boolean | Whether WHOIS/RDAP data is redacted for privacy (GDPR) |
| contacts | object | Contact information by role (null when no data available, fields may be null when redacted) |
Example
curl "https://domani.run/api/domains/whois?q=google.com"
/api/domains/{domain}/ogPublicGet website preview metadata for a domain
Returns Open Graph metadata (title, description, image, favicon) for a domain. Data is lazily fetched and cached for 7 days. First request for a domain may take 1-5s (live fetch); subsequent requests are instant (cache hit). Returns null fields if the site is unreachable or has no OG tags. Public endpoint - no authentication required.
Parameters
| domain* | string | Domain to fetch metadata for (e.g. google.com) |
Response 200
| title | string | null | Page title (og:title > twitter:title > <title>) |
| description | string | null | Page description (og:description > meta description) |
| image | string | null | OG image URL (absolute) |
| favicon | string | null | Favicon URL (absolute) |
Example
curl "https://domani.run/api/domains/google.com/og"
Billing
/api/billing/setupGet a URL for adding a payment method
Always returns both a Stripe Checkout url and a SetupIntent client_secret for adding a credit card. An agent that cannot run Stripe.js should open url in a browser and tell the user to add their card there; a frontend embedding Stripe Elements directly (such as the Domani dashboard) uses client_secret instead. This is required before purchasing domains with a card. After the user completes checkout, they are redirected back to the dashboard.
Request body
| mode | string(setup_intent | checkout) | Deprecated: both `url` and `client_secret` are always returned regardless of this value. Kept for backward compatibility. |
Response 200
| url | string (uri) | Stripe Checkout URL - open this in a browser to add a card |
| client_secret | string | Stripe SetupIntent client secret, for a frontend using Stripe.js directly |
| hint | string | Next steps guidance |
Example response
{
"url": "https://checkout.domani.run/session/cs_abc123...",
"client_secret": "seti_abc123_secret_xyz",
"hint": "Open `url` in a browser to add your payment card."
}Example
curl -X POST https://domani.run/api/billing/setup \ -H "Authorization: Bearer domani_sk_xxx"
/api/billing/cardRemove saved payment method
Detaches the user's saved credit card from their account. After removal, a new card must be added before purchasing domains.
Response 200
| removed | boolean |
Example response
{
"removed": true
}Example
curl -X DELETE https://domani.run/api/billing/card \ -H "Authorization: Bearer domani_sk_xxx"
/api/billing/connectGet marketplace payout status
Returns the seller's Stripe Connect onboarding status and available marketplace balance. payouts_enabled is true once onboarding is complete and the seller can receive bank payouts.
Response 200
| onboarded | boolean | Seller has submitted Connect onboarding details |
| payouts_enabled | boolean | Bank payouts are active |
| details_submitted | boolean | |
| balance_cents | integer | Available marketplace balance in cents |
Example response
{
"onboarded": true,
"payouts_enabled": true,
"details_submitted": true,
"balance_cents": 45000
}Example
curl https://domani.run/api/billing/connect \ -H "Authorization: Bearer domani_sk_xxx"
/api/billing/connectStart bank payout onboarding
Creates (or resumes) Stripe Connect Express onboarding for a marketplace seller and returns a hosted onboarding URL to redirect the user to. Required before a seller can withdraw earnings to their bank.
Response 200
| url | string (uri) | Stripe-hosted onboarding URL |
Example response
{
"url": "https://connect.stripe.com/setup/e/acct_123/..."
}Example
curl -X POST https://domani.run/api/billing/connect \ -H "Authorization: Bearer domani_sk_xxx"
/api/billing/connect/payoutWithdraw marketplace balance to bank
Transfers the seller's entire available marketplace balance to their connected bank account. Requires completed payout onboarding (POST /api/billing/connect). The balance is reserved before the transfer and restored if it fails, so a failure never loses funds.
Response 200
| amount_cents | integer | Amount transferred in cents |
| transfer_id | string | Stripe transfer id |
Example response
{
"amount_cents": 45000,
"transfer_id": "tr_123"
}Example
curl -X POST https://domani.run/api/billing/connect/payout \ -H "Authorization: Bearer domani_sk_xxx"
/api/billing/invoicesList payment invoices
Returns a list of paid invoices with invoice number, amount, date, and links to view or download the PDF. Invoices are generated automatically for every domain purchase, renewal, and transfer.
Parameters
| limit | integer | Max invoices to return |
Response 200
| invoices | object[] | |
| has_more | boolean | |
| hint | string |
Example
curl https://domani.run/api/billing/invoices \ -H "Authorization: Bearer domani_sk_xxx"
Referrals
/api/referralsGet referral earnings and history
Returns your unique referral code, commission rate (20%), earnings breakdown (total, paid, pending), and a history of all referred purchases. Share your referral code with others - when they register with your code (via the ref parameter), you earn a commission on all their future domain purchases automatically.
Response 200
| referral_code | string | Your unique referral code to share |
| referral_rate | integer | Commission percentage (currently 20%) |
| total_earned_cents | integer | Total earnings in cents |
| total_paid_cents | integer | Total paid out in cents |
| total_pending_cents | integer | Pending payout in cents |
| referrals | object[] |
Example response
{
"referral_code": "x7k9m2",
"referral_rate": 20,
"total_earned_cents": 400,
"total_paid_cents": 200,
"total_pending_cents": 200,
"referrals": [
{
"id": "cm5ref001",
"domains": [
"friend-site.com"
],
"earned_cents": 200,
"paid_out": true,
"created_at": "2025-01-20T10:00:00.000Z"
},
{
"id": "cm5ref002",
"domains": [
"another.dev"
],
"earned_cents": 200,
"paid_out": false,
"created_at": "2025-02-10T15:30:00.000Z"
}
]
}Example
curl https://domani.run/api/referrals \ -H "Authorization: Bearer domani_sk_xxx"
Backorders
/api/backordersPlace a backorder on a taken domain
Watches a domain that is currently registered to someone else and automatically registers it for you when it becomes available (drops) - availability is polled every few minutes, so the catch lands within minutes of the drop. You are charged only if and when the catch succeeds - no upfront fee. A price cap protects you from a registration-price spike between now and the drop. Requires a payment method on file (card) or payment_method: "balance" so the catch can be paid when it happens. Best-effort: a contested drop may be taken by a specialized drop-catcher first.
Request body
| domain* | string | The currently-registered domain to watch and catch on drop. |
| max_price | number | Max USD you'll pay to catch it. Defaults to the current registration price. Must be >= the current price. |
| payment_method | string(card | balance) | How to pay when caught. Defaults to your preferred method. |
Response 201
| backorder | Backorder | |
| hint | string |
Example
curl -X POST https://domani.run/api/backorders \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"domain": "taken.com"}'/api/backordersList your backorders
Returns your backorders, newest first. Filter by status (watching, caught, failed, cancelled, expired).
Parameters
| status | string(watching | caught | failed | cancelled | expired) | Filter by status. |
Response 200
| backorders | Backorder[] |
/api/backorders/{id}Get a backorder
Parameters
| id* | string | Backorder ID. |
Response 200
| backorder | Backorder |
/api/backorders/{id}Cancel a backorder
Stops watching a domain. Only backorders in the watching state can be cancelled.
Parameters
| id* | string | Backorder ID. |
Response 200
| id | string | |
| status | string | |
| hint | string |
Marketplace
/api/domains/sellList a domain for sale (open marketplace)
List any domain for sale on the Domani marketplace, including domains on other registrars. Domains already in your Domani account are auto-verified and listed immediately (status active). External domains require TXT ownership verification (status pending_verification). Fees: 10% platform commission on successful sales. Requires deals:write scope.
Request body
| domain* | string | Domain name to list for sale |
| price* | number | Sale price in USD |
| description | string | Optional listing description |
Response 201
| listing | object | |
| txt_record | object | null | TXT record to add for ownership verification (only for external domains) |
| hint | string |
/api/domains/sell/verifyVerify TXT ownership for external listing
Verify that the TXT ownership record has been added for an external domain listing. On success, the listing status changes from pending_verification to active and becomes buyable. Requires deals:write scope.
Request body
| domain* | string | Domain name to verify |
Response 200
| listing | object | |
| hint | string |
/api/domains/sell/statusCheck listing and deal status
Check the status of a domain listing and any active deal. Returns listing details, deal pipeline status, and contextual hints. Requires deals:read scope.
Parameters
| domain* | string | Domain name to check |
Response 200
| listing | object | null | |
| deal | object | null | |
| hint | string |
/api/dealsList your deals
List marketplace deals where you are the buyer or seller. Filter by role or status. Requires deals:read scope.
Parameters
| role | string(buyer | seller) | Filter by role |
| status | string(active | escrow_held | epp_pending | transfer_started | completed | refunded | expired) | Filter by status ('active' = all non-terminal) |
| domain | string | Filter by domain name |
| limit | integer | Number of deals to return (default 50, max 100) |
| cursor | string | Pagination cursor from a previous response |
Response 200
| deals | object[] | |
| has_more | boolean | Whether more results are available |
| next_cursor | string | null | Cursor for the next page |
/api/dealsPurchase an external domain (create deal)
Create an escrow deal to purchase a domain listed on the marketplace. Buyer's payment is held in escrow until the domain transfer completes. The seller is notified and has 7 days to provide the EPP/auth code. Requires domains:transfer scope.
Request body
| domain* | string | Domain name to purchase |
| payment_method | string(card | usdc | balance) | Payment method: 'card' (default), 'usdc', or 'balance' (marketplace credit). |
Response 201
| deal | object | |
| hint | string | |
| next_steps | string[] |
/api/deals/{id}Get deal detail with timeline
Get full details of a deal including price breakdown, deadlines, and event timeline. Both buyer and seller can access their deals. Requires deals:read scope.
Parameters
| id* | string | Deal ID |
Response 200
| deal | object | |
| hint | string |
/api/deals/{id}Provide EPP/auth code (seller)
Seller provides the EPP/authorization code to initiate the domain transfer. The code is encrypted at rest and used to start the registrar transfer. Max 3 attempts. Requires deals:write scope.
Parameters
| id* | string | Deal ID |
Request body
| auth_code* | string | EPP/authorization code from the current registrar |
Response 200
| deal | object | |
| hint | string |
/api/deals/{id}/invoiceGet a deal receipt / statement
Returns a role-aware invoice for a marketplace deal. Buyers get a purchase receipt; sellers get a statement showing the sale, the platform commission line, and the net payout. Works for every payment method (card/USDC/balance). Requires deals:read scope.
Parameters
| id* | string |
Response 200
| invoice | DealInvoice |
/api/negotiationsList your negotiations
Lists price negotiations where you are the buyer or seller. Counterparties are anonymized (roles only). Requires deals:read scope.
Parameters
| role | string(buyer | seller) | Filter by your role |
| status | string | Filter by status (open|agreed|completed|declined|expired|cancelled|all) |
Response 200
| negotiations | Negotiation[] |
/api/negotiationsMake an offer on a listing
Opens an anonymous price negotiation on an actively-listed domain with an initial offer. The seller can counter, accept, or decline. On acceptance the buyer finalizes payment and a deal is created at the agreed price. Requires deals:write scope.
Request body
| domain* | string | The listed domain to negotiate on |
| offer* | number | Initial offer in USD |
| message | string | Optional message to the seller |
Response 201
| negotiation | Negotiation |
Example
curl -X POST https://domani.run/api/negotiations \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"domain":"example.com","offer":500}'/api/negotiations/{id}Get a negotiation thread
Returns a negotiation with its full offer thread, anonymized (offers are labelled 'you' or 'counterparty'). Requires deals:read scope.
Parameters
| id* | string |
Response 200
| negotiation | Negotiation |
/api/negotiations/{id}Counter, accept, or decline
Respond to the current offer on the table when it's your turn. counter requires a new amount; accept locks the price at the amount currently on the table; decline ends the negotiation. Requires deals:write scope.
Parameters
| id* | string |
Request body
| action* | string(counter | accept | decline) | |
| counter | number | New amount in USD (required when action=counter) |
| message | string |
Response 200
| negotiation | Negotiation |
/api/negotiations/{id}Withdraw a negotiation
Buyer withdraws an open or agreed negotiation. Requires deals:write scope.
Parameters
| id* | string |
Response 200
| cancelled | boolean |
/api/negotiations/{id}/finalizeFinalize an agreed negotiation (pay)
Buyer pays the agreed price to create the escrow deal, then the standard transfer flow runs. Accepts the same payment methods as a marketplace purchase (card, USDC, x402). Returns 402 with payment instructions if no payment is provided. For a broker-sourced acquisition, if the agreed price is above the max_budget you set, returns 409 BUDGET_EXCEEDED - re-submit with confirm_over_budget: true to proceed. Requires domains:transfer scope.
Parameters
| id* | string |
Request body
| payment_method | string(card | usdc | balance) | |
| payment_signature | string | null | |
| payment_tx | string | |
| payment_chain | string | |
| confirm_over_budget | boolean | For broker deals: proceed even though the agreed price exceeds the max_budget you set. |
/api/brokerList your broker requests
Lists your domain acquisition requests and their status. Requires deals:read scope.
Parameters
| status | string | Filter by status or 'all' |
Response 200
| requests | BrokerRequest[] |
/api/brokerRequest acquisition of a taken domain
Ask domani to acquire a specific taken, unlisted domain on your behalf. Agents source the owner via RDAP, reach out anonymously, and negotiate - commission-only, no upfront fee. Owner interest opens an anonymous negotiation. Note: many owners are unreachable (GDPR-redacted WHOIS) - the request then ends no_contact. Requires deals:write scope.
Request body
| domain* | string | The taken domain you want |
| max_budget | number | Your ceiling in USD (optional) |
Response 201
| request | BrokerRequest |
Example
curl -X POST https://domani.run/api/broker \
-H "Authorization: Bearer domani_sk_xxx" \
-H "Content-Type: application/json" \
-d '{"domain":"dream.com","max_budget":5000}'/api/broker/{id}Get a broker request
Returns one of your broker requests. Requires deals:read scope.
Parameters
| id* | string |
Response 200
| request | BrokerRequest |
/api/broker/{id}Cancel a broker request
Cancels an active domain acquisition request. Requires deals:write scope.
Parameters
| id* | string |
Response 200
| cancelled | boolean |
/api/broker/respondPublicRead a broker inquiry's state (owner side)
The owner-agent read path: poll the current, anonymized state of an inquiry with your token before acting. Returns the domain, the buyer's offer on the table, whose move it is, and the actions available now. Never exposes the buyer's identity or budget. Token-authorized.
Parameters
| token* | string | The outreach token from your inquiry email. |
Response 200
| domain | string | |
| status | string | |
| your_turn | boolean | Whether it's the owner's move. |
| current_offer_usd | number | null | The buyer's offer on the table. |
| agreed_price_usd | number | null | |
| actions | string[] | Actions available now (subset of offer/counter/accept/decline/opt_out). |
Example
curl "https://domani.run/api/broker/respond?token=<from your inbox>"
/api/broker/respondPublicRespond to a broker inquiry (owner side)
The owner-agent path: respond to a broker's acquisition inquiry using the token from the outreach email delivered to your inbox - the fully autonomous alternative to replying by email or clicking the human claim link. offer/counter open or advance an anonymous negotiation at your asking price; decline and opt_out end it. Token-authorized (the token is the capability) - no account or scope required.
Request body
| token* | string | The outreach token from your inquiry email (the reply-to is broker+<token>@). |
| action* | string(offer | counter | accept | decline | opt_out) | offer/counter to name a price; accept to take the buyer's current offer on the table; decline to refuse; opt_out to never be contacted again. |
| price | number | Your asking price in USD (required for offer/counter). |
Response 200
| handled | boolean | |
| outcome | string | opted_out | declined | negotiation_opened | countered | ignored_not_your_turn | unclear |
| status | string | |
| negotiation_id | string | |
| domain | string |
Example
curl -X POST https://domani.run/api/broker/respond \
-H "Content-Type: application/json" \
-d '{"token":"<from your inbox>","action":"offer","price":800}'Auth
/api/oauth/registerRegister an OAuth client (RFC 7591)
Dynamic client registration for OAuth-capable MCP clients (Claude.ai, ChatGPT, Grok Bot connectors). Open registration: it grants nothing by itself - every authorization still passes the user consent screen. Public clients (token_endpoint_auth_method=none) must use PKCE S256. Discovery: /.well-known/oauth-authorization-server.
Request body
| redirect_uris* | string[] | Exact redirect URIs (https, http on loopback, or a native custom scheme) |
| client_name | string | Shown on the consent screen |
| token_endpoint_auth_method | string(none | client_secret_post | client_secret_basic) | |
| client_uri | string | |
| logo_uri | string |
Response 201
| client_id | string | |
| client_name | string | |
| redirect_uris | string[] | |
| token_endpoint_auth_method | string | |
| grant_types | string[] | |
| response_types | string[] | |
| client_id_issued_at | integer | |
| client_secret | string | Only for confidential clients; returned once |
/api/oauth/tokenOAuth token endpoint
Exchange an authorization code (with PKCE code_verifier) for an access token, or rotate with grant_type=refresh_token. Access tokens are ordinary domani API tokens carrying the approved scopes and mailbox confinement. Codes are single-use; refresh tokens rotate on every use.
Response 200
| access_token | string | |
| token_type | string | |
| expires_in | integer | |
| refresh_token | string | |
| scope | string |