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 fromohmyhost 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.
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. Follownext_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
UsecreateTransactionalMailClient 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:
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.-
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 callsverifyMailWebhookfrom@ohmyhost/customer-runtimebefore parsing JSON. Foremail.received, it stores the message in your own database keyed by the stable eventid, commits, and only then returns 2xx. -
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_SECRETand redeploy (OHMYHOST_*names are reserved): -
Verify the deployed handler. A signed
email.webhook_testmust return 2xx without storing a real message. Verification turns receiving on; then apply the returned MX records and readmail statusuntil receiving isready:
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: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.