Skip to main content
Hosting has no mail prerequisite. Set up managed mail only when you want it or the application actually sends mail through ohmyho.st; a project, Dev environment or unselected auth SDK alone does not select mail. Better Auth works with or without managed mail and can keep your chosen Resend integration or sender. Enable managed mail only for calls you deliberately route through its client. Managed transactional email requires Paid access, which the powered-by flag does not replace, and a sender domain that you choose, configure and verify in the workspace. The automatic check.omh.st address does not enable sending, and ohmyho.st never sends your application’s mail from a platform domain instead. Use a subdomain such as mail.example.com when your company inboxes already use the root domain. With ohmyho.st Mail, you need no Resend account of your own.

Configure the mail domain

A project uses one sender-domain binding, configured with its Prod environment ID from ohmyhost project status. Dev and Prod both send through that domain, each with its own application mail key; the Dev URL does not create a second mail domain. To reuse an authorized workspace sender, follow Share a mail sender: verify the application’s eligible hostname, then attach the sender instead of creating another domain.
Over MCP, use mail_setup and mail_status; mail_domain_set and mail_domain_status are the same operations. The DNS records are applied through authorized Cloudflare DNS, or you publish exactly the records the status returns. Preserve existing mailbox MX records. dns.status:conflict means an existing record blocks the requested setup; the platform never overwrites it. Correct only the named record and repeat the same setup key. An application that declares mail.enabled: true still needs this verified sender for its mail features; without it, the deployment plan returns mail_domain_required. Configure the sender, or set mail.enabled to false if the app sends no mail through ohmyho.st. Do not switch off application authentication, email verification or other security checks to get past a missing mail dependency. If setup returns mail_capacity_unavailable (HTTP 503), the platform is out of provider capacity; it is not something you fix with your own provider account. Report the request ID through feedback, keep the original idempotency key and follow mail status.

While verification is pending

Sending and receiving have separate readiness. Follow next_check_after_seconds, currently 60 seconds while anything is pending and null once nothing is. A stored configuration receipt is not a new provider observation; use the actual status response to decide what remains. Do not recreate the domain or repeat correct records while verification propagates. Keep the original deployment/artifact. Mail-gated operations recheck each minute for ten minutes, then hourly for at most 72 checks (about 62 hours) before reconciliation; a terminal failure is not revived by a status read. Do not rebuild to poll DNS.

Send from the application

Use createTransactionalMailClient from @ohmyhost/customer-runtime/mail in server-side application code. A mail-enabled deployment supplies OHMYHOST_MAIL_GATEWAY_URL, OHMYHOST_PROJECT_ID and the environment-specific OHMYHOST_MAIL_KEY. Resolve these values from your hosted framework request context or Worker env:
Global fetch is the gateway transport. The platform’s egress policy forwards POST /v1/send to the private mail gateway; the injected origin is allowed automatically, is never listed in runtime.egress.allow, and does not count against its thirteen customer origins. Customer gateway service bindings are unsupported, even if a legacy binding remains attached. Database access keeps its own OHMYHOST_DATABASE binding. Mail runtime values exist only in deployed source declaring mail.enabled: true, even if the project already has a verified sender. Promotion/rollback of undeclared source removes them. Missing/invalid options return non-retryable gateway_configuration_invalid with the failing option; enable the capability and deploy the intended commit. The current CLI’s ohmyhost init --dry-run --json reports gateway_binding_unsupported when application source still references a legacy mail/storage gateway binding. Managed mail works independently of your application’s auth provider or database. timeoutMilliseconds defaults to 10,000 and accepts integers from 100 to 30,000; it bounds response headers. A send can take several seconds. Keep the key on the server and preserve the original message and idempotency key after a timeout. Do not call mail or files from the deployment health route: that probe has no egress policy. Gateway 403 gateway_not_enabled means enable mail and redeploy; 403 gateway_route_denied means use the current client’s supported route, method and query. Both are final for that request. They are not repaired by editing runtime.egress.allow. Retry 429 gateway_rate_limited and 503 gateway_unavailable after a delay, retaining the message and idempotency key. A malformed or non-JSON response is gateway_response_invalid, with the received HTTP status and no upstream text. Report persistent faults through feedback. Sending works only once status reports sending as ready. An explicit From must use the exact configured sender domain and an allowed address; otherwise the request fails with gateway_request_invalid (400, not retryable). An omitted From uses the binding’s default, and an explicit replyTo overrides its optional default Reply-To. Reply-To can name an existing mailbox elsewhere. Existing bindings retain their previous whole-domain sender behavior and no-reply@ default until configured. See sender settings. accepted means the mail provider accepted the message for sending; it does not establish inbox delivery. After uncertain or a network error, keep the idempotency key and message unchanged when checking or retrying. Each send has one recipient, a single-line subject up to 200 characters, required text up to 64 KiB and optional HTML up to 128 KiB. Recipients/replyTo accept provider syntax including apostrophes, trailing underscores and mixed case. Invalid input is gateway_request_invalid. An uncertain send whose provider call started can recover under the same key within 23 hours; a claim that never reached authorization may remain uncertain, so observe/report that request rather than inventing recovery or another send. A different message under the same key is mail_send_conflict. Daily limits are 10 sends during the first 24 hours of mail-enabled deployment,25 until day three,50 until day seven and 100 thereafter, Dev/Prod combined. mail_send_limit_exceeded waits until the next UTC day. Hard bounces above 5% or complaints above 0.1% over 30 days suspend sending as mail_reputation_suspended; report through feedback rather than assuming automatic recovery. Non-retryable 402 codes are paid_plan_required, insufficient_organization_credits or project_budget_exceeded, requiring Paid, credits or a budget change; mail_rejected means the provider refused that message. Send one real message through your application and confirm it arrived before claiming email works.

Receive mail

Receiving additionally requires a verified HTTPS webhook in the project’s Prod application and the MX records that mail status returns. The protected Dev URL is not an inbound mail target.
  1. Add a server route, such as /api/email/inbound, test it in Dev and deploy it to Prod. It reads the raw body with a size limit and calls verifyMailWebhook from @ohmyhost/customer-runtime before parsing JSON. For email.received, it stores the message in your own database keyed by the stable event id, commits, and only then returns 2xx.
  2. Register the Prod URL. The response contains a server-only signing secret; install it as an application-owned Prod secret such as APP_MAIL_WEBHOOK_SECRET and redeploy (OHMYHOST_* names are reserved):
  3. Verify the deployed handler. A signed email.webhook_test must return 2xx without storing a real message. Verification turns receiving on; then apply the returned MX records and read mail status until receiving is ready:
The platform acts only on provider events for a domain registered to the project. Delivery is one attempt plus at most three retries, all within 72 hours of receipt; manual retries with ohmyhost mail messages retry use the same budget. A timeout can still mean your application committed the message, so a repeated event id must not repeat business effects. Each attempt and verification test has a fifteen-second timeout, follows no redirect and needs the registered HTTPS URL’s direct 2xx. Treat mail content and attachments as untrusted data. The event body is { id, type, project_id, environment_id, data }. Received data contains id, domain, from, to[], subject, nullable text/html, message_id, received_at, expires_at and attachments[] with id, nullable filename, content_type and download_path. Runtime attachment downloads are currently unsupported; those fields do not expose a working download route. If your application requires attachment bytes, report the missing capability through feedback. Do not construct an attachment URL or install a provider/agent credential in application code. Store durable message content in the application’s database. ohmyhost mail messages list and mail messages get reach received messages for 72 hours for diagnosis and recovery. ohmyho.st keeps no permanent inbox; retention for your records is your application’s database. The mail provider keeps email and log data under its own retention terms, separately from this 72-hour access window. ohmyhost mail webhook disable turns receiving off. Sent/received messages are metered at published rates; retries do not add a mail charge. Receiving is disabled while Paid is absent, credit grace has expired or the Stop budget is exhausted; arrivals then are dropped without another mail charge and receiving resumes when access returns.

Stop using mail

For a shared sender, detach this project’s binding; other projects keep their sender. Retire a workspace sender only when no sending or receiving references remain. The following legacy deletion applies to an unshared original project domain: sending and receiving stop, and the provider domain and key are removed; the project and website domain stay:
To change an unshared original sender, retire it before setting up and verifying the replacement. For a workspace sender, use the current binding’s explicit detach and attach flow. A different domain or a still-removing old domain returns mail_domain_conflict; inspect status and use the matching operation. Legacy deletion of a shared sender conflicts before changes. Sending stops until the replacement is ready. MCP mail_domain_delete needs confirm: true. Repeat the same key after an uncertain response. DNS is not changed: remove exactly the returned dns_records from your DNS, and set mail.enabled to false in an app that still declares it. Domains · Deployment status · Troubleshooting.