Email Relay
Betamail.vappcdn.com
Transactional and mailbox email, without renting someone else's reputation.
Compatible with SMTP · IMAP · JMAP
- Standards all the way down — SMTP, IMAP, JMAP. No proprietary client.
- SPF, DKIM and DMARC configured per domain, signed automatically.
- Transactional send API for receipts, alerts, and password resets.
- Delivery webhooks so your app knows what actually landed.
Usage guide
1 · Your API key, and the call format
Every call is a POST with a bearer token, sent to the origin below. Keep the origin in one config value rather than scattered through your code.
bash# API origin: https://cdn.vapp.vn
curl -sX POST https://cdn.vapp.vn/app/rpc/account/me \
-H "authorization: Bearer $VAPPCDN_API_KEY" \
-H 'content-type: application/json' \
-d '{"json":{}}'
# The {"json": …} envelope is required going IN as well as OUT.
# success: {"json": …}
# error: {"json":{"code":"FORBIDDEN","status":403,"message":"…"}}2 · Register a sending domain
Use a subdomain, not your apex: it isolates sending reputation from the domain that carries your website and your DNS. We follow the same rule ourselves.
bashcurl -sX POST https://cdn.vapp.vn/app/rpc/mail/addDomain \
-H "authorization: Bearer $VAPPCDN_API_KEY" \
-H 'content-type: application/json' \
-d '{"json":{"domain":"send.example.com","brandDomain":"example.com"}}'
# → expectations: { dkimSelector, spfMechanism, dkimRecord }
# Publish the selector WE return, not one you choose.3 · Publish three records, then verify
SPF names a HOSTNAME, not an IP — so when our sending infrastructure moves, you change nothing. If you already have an SPF record, ADD the mechanism to it: two SPF records is a permanent error at every receiver.
dnssend.example.com TXT "v=spf1 a:mail.vappcdn.com ~all"
<selector>._domainkey.send.example.com TXT "v=DKIM1; k=rsa; p=…"
_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
# then, until status is "active":
POST /app/rpc/mail/checkDomain {"json":{"domain":"send.example.com"}}
# Per-record status: ok · missing · invalid · unknown
# "unknown" means DNS did not answer us. It is NOT a failure — poll again.
# We check through a public resolver, never our own nameservers.4 · Mint a submission credential
One credential is one reputation: quota, warmup and suppression are all keyed on it. One credential per sending identity, and never reuse one across products.
bashcurl -sX POST https://cdn.vapp.vn/app/rpc/mail/createCredential \
-H "authorization: Bearer $VAPPCDN_API_KEY" \
-H 'content-type: application/json' \
-d '{"json":{"domain":"send.example.com","name":"prod","localPart":"noreply"}}'
# → {"login":"noreply@send.example.com","secret":"…"}
# The secret is returned ONCE. There is no way to read it back.5 · Send, and read the per-recipient outcome
The result is per recipient, because the outcomes genuinely differ. A caller that reads only the HTTP code records a deferred address as sent.
bashcurl -sX POST https://cdn.vapp.vn/app/rpc/mail/send \
-H "authorization: Bearer $VAPPCDN_API_KEY" \
-H 'content-type: application/json' \
-d '{"json":{"from":"noreply@send.example.com","fromName":"Example",
"to":["someone@example.org"],
"subject":"Confirm your email",
"text":"Confirm here: https://example.com/confirm?t=…"}}'
# → {"messageCount":1,
# "outcomes":[{"to":"someone@example.org","status":"accepted","code":250}]}
#
# accepted we took responsibility for it — NOT the same as delivered
# deferred temporary; retry later, nothing is lost
# refused permanent; do not retry
# suppressed never left the building (suppression list, or an unverified domain)
#
# Each recipient gets its own message: we never put several addresses on one
# To:, which would disclose your recipient list to everyone on it.6 · Receiving — forwarding
Receiving needs MX pointed at us; sending does not. Do not point MX at us while another provider still handles your mail.
bash# catch-all: everything at the domain lands in one mailbox
POST /app/rpc/mail/addForward
{"json":{"domain":"example.com","kind":"catchall","destEmail":"you@example.net"}}
# one address
POST /app/rpc/mail/addForward
{"json":{"domain":"example.com","kind":"external",
"source":"support","destEmail":"team@example.com"}}
# Forwarded mail is re-sent by us, so the envelope sender is rewritten. You do
# not configure that; it is what keeps SPF passing at the final destination.Endpoints and error codes
Every endpoint is POST /app/rpc/<namespace>/<procedure> — no path parameters, no query strings. Sending to arbitrary recipients opens up once your sending domain reaches active; before that the API is working correctly when it returns suppressed.
| name | accepts | notes |
|---|---|---|
| mail/addDomain | domains | Registers a sending domain and returns the records to publish. |
| mail/checkDomain | domains | Checks SPF, DKIM and DMARC through a public resolver. |
| mail/refreshDkim | domains | Re-fetches the DKIM record if the first call had none. |
| mail/createCredential | senders | Mints a submission credential. The secret is shown once. |
| mail/send | send | Sends a message. The result is per recipient. |
| mail/addForward | receive | Catch-all or single-address forwarding. |
| mail/listForwards | receive | Lists the forwarding rules on a domain. |
| mail/removeForward | receive | Removes a forwarding rule. |
| 401 UNAUTHORIZED | error | No key, or the key is revoked. Do not retry. |
| 403 FORBIDDEN | error | Policy said no — unverified domain, or a plan limit. Never retryable. |
| 404 NOT_FOUND | error | Does not exist, OR is not yours — deliberately not distinguished. |
| 429 TOO_MANY_REQUESTS | error | Deferred, or rate limited. Retry with backoff — this one you should. |
| 503 SERVICE_UNAVAILABLE | error | An outage on our side. Nothing was sent. Retry. |