Developers → Guides

Receive email in your app

Support tickets, replies to notifications, documents mailed to a bot: mail to an address on your domain arrives at your HTTPS endpoint as one signed JSON POST, with the text, the HTML and every attachment. No mailbox to poll and no IMAP.

What you need

  • A domain registered at NameOcean: forwarding and mail to webhook are free for those.
  • An HTTPS endpoint on a public address that answers within 20 seconds.
  • The nameocean CLI, or an API key with email:write.

1. Turn on forwarding for the domain

nameocean
$ nameocean email forwards on example.com

This points the domain's MX record to NameOcean's mail forwarders. If the domain already has MX records, for mailboxes somewhere else, the command stops and lists them: all mail to the domain would move here. Add --replace-mx only when that is what you want.

2. Ask for mail to webhook

Posting mail to a URL is opened per account after a short review, to keep spam relays out. On the domain's page under Dashboard → Email Forwarding, ask for it with a line on what you will use it for. Until then the API answers 403 not_approved.

3. Send an address to your endpoint

nameocean
$ nameocean email inbound add example.com support https://app.example.com/hooks/mail
curl
$ curl -X POST https://nameocean.net/api/v1/email/forwarding/example.com/webhooks \
    -H "Authorization: Bearer $NAMEOCEAN_API_KEY" -H "Content-Type: application/json" \
    -d '{"localpart": "support", "url": "https://app.example.com/hooks/mail"}'

The answer carries the signing secret (whsec_…); keep it in your app's settings. localpart * sends every address of the domain that has no alias of its own. A domain can have up to three of these webhooks.

4. Receive it

Verify the signature over the raw body, then read the JSON. A small Flask receiver:

app.py
import base64, hashlib, hmac, json, os, time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["NAMEOCEAN_WEBHOOK_SECRET"]  # whsec_...


def verified(header, body, tolerance=300):
    fields = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
    ts = int(fields.get("t", "0"))
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, fields.get("v1", ""))


@app.post("/hooks/mail")
def mail():
    body = request.get_data()
    if not verified(request.headers.get("X-NameOcean-Signature", ""), body):
        abort(401)
    mail = json.loads(body)
    print(mail["envelope"]["from"], "→", mail["envelope"]["to"], mail["subject"])
    for a in mail["attachments"]:
        data = base64.b64decode(a["content_base64"])
        print("  attachment", a["filename"], a["content_type"], len(data), "bytes")
    return "", 204  # any 2xx: delivered

What arrives

POST https://app.example.com/hooks/mail
X-NameOcean-Event: email.received
X-NameOcean-Delivery: 6f1c2a4e-8b3d-4f5a-9c7e-1d2b3a4c5e6f
X-NameOcean-Signature: t=1794300000,v1=5f2b…

{
  "id": "6f1c2a4e-8b3d-4f5a-9c7e-1d2b3a4c5e6f",
  "event": "email.received",
  "received_at": "2026-11-11T09:30:12+00:00",
  "envelope": {"from": "alice@example.org", "to": "support@example.com"},
  "from": "Alice <alice@example.org>",
  "to": "support@example.com",
  "cc": null,
  "reply_to": null,
  "subject": "Order 1042",
  "date": "Wed, 11 Nov 2026 09:30:05 +0000",
  "message_id": "<…@example.org>",
  "in_reply_to": null,
  "references": null,
  "text": "Hi…",
  "html": "<p>Hi…</p>",
  "headers": [["From", "Alice <alice@example.org>"], …],
  "attachments": [{"filename": "invoice.pdf", "content_type": "application/pdf", "content_id": null,
                   "size": 48211, "content_base64": "JVBERi0…"}],
  "spam_score": 0.4,
  "size": 66120
}

id stays the same when a mail is posted again, so use it to skip duplicates. in_reply_to and references tell you which of your own mails someone answered.

When your endpoint is down

  • A 2xx answer means delivered. A 408, 429, 5xx, a timeout (20 seconds) or no connection is retried for 6 hours.
  • Any other 4xx is final: the mail is not posted again by itself.
  • Mail that could not be posted is kept for 30 days. Find it with nameocean email forwards log example.com (or GET /email/forwarding/example.com/messages) and release it: it is posted once more, with the same id.
  • Up to 500 mails a day per domain go to webhooks; mail over that is held the same way.