Email Services & API for Private Clients
Agent API & MCP
Authenticate as an agent with an API key, send mail via HTTPS or MCP JSON-RPC,
and read credits. People signed in to the web app are not charged.
An agent requests its own key with request_access or POST /api/requestAccess.
from to an address on a verified domain you own, for example support@example-domain.com. If it is omitted and the account has exactly one sendable hosted mailbox, that mailbox is used as a fallback. The shared agent key sends as noreply@mailservicenow.com. You call HTTPS only. Inbound MX is mail.mailservicenow.com. The shared inbox@mailservicenow.com mailbox stays with Mail Service Now. A hosted mailbox on a verified domain opens in the web app.
Authentication
| Caller | Credential | How to send |
|---|---|---|
| Agent | API key msn_<redacted> |
Authorization: Bearer msn_…or X-Api-Key: msn_…
|
On sendMail, if the Bearer token looks like a Firebase JWT it is
verified with verifyIdToken; otherwise it is treated as the agent API key.
Mail routes accept that JWT only for an admin account. Any other Firebase user is
403 This mailbox requires an operator account.
request_access and verify_access_request need no key.
Other MCP tools use an agent API key. Mailbox tools also accept a Firebase ID token once account setup is complete.
Agents request their own key. Call request_access (MCP) or
POST /api/requestAccess with contactEmail, domain,
agentName, and intendedUse. The contact address is emailed an
8-digit code. Then call verify_access_request (or
POST /api/verifyAccessRequest) with requestId and code.
A domain on the domain list returns a standard key once. A domain that is not on that list
stays setting_up. Mail Service Now only finishes those requests that are still being set up.
It does not hand an agent its key. Never commit or paste a full key into a public channel.
The same steps are in the Markdown guide (How an agent requests access)
and llms.txt.
Credits model
- Each successful agent send costs 1 credit.
- The default agent key starts at 100 credits
(Firestore
agent_credits/default.remaining). - People signed in to the web app are not charged.
- Empty balance: HTTP 429 with
errorInsufficient credits(not 402). - Rate limit on send: 5/minute and 50/UTC-day per caller. Same 429 status,
errorRate limit exceeded. The agent key shares one bucket.
Endpoints
| Name | URL |
|---|---|
| sendMail | https://mailservicenow.com/api/sendMail |
| MCP (JSON-RPC) | https://mailservicenow.com/api/mcp |
| inbox | https://mailservicenow.com/api/inbox |
| OpenAPI-ish | /docs/openapi.json |
sendMail (REST)
POST JSON body: to, subject, text, and from. Optional attachments (max 3, 2MB each, 4MB total). Always set from to an address on a verified domain you own, for example support@example-domain.com. If it's omitted and the account has exactly one sendable hosted mailbox, that mailbox is used as a fallback; with more than one, omitting from is an error. The shared agent key sends as noreply@mailservicenow.com. MCP send_mail uses the same from rule and does not accept attachments.
Success: { "ok": true, "messageId": "<…>", "creditsRemaining": N }
(creditsRemaining only for agent auth).
Example: agent send (curl)
curl -sS -X POST \
https://mailservicenow.com/api/sendMail \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"to": "recipient@example.com",
"from": "support@example-domain.com",
"subject": "Hello from an agent",
"text": "Sent via MailServiceNow agent API."
}'
How an agent requests access
No API key is required. The code is emailed and is not in the response. The first response does not say whether an account is already on file.
- Call
request_accessorPOST /requestAccesswithcontactEmail,domain,agentName, andintendedUse. - Wait for the email. The code is 8 digits and expires in 24 hours.
- Call
verify_access_requestorPOST /verifyAccessRequestwithrequestIdandcode. -
status: "ready"includesaccess: "standard"andapiKeyonce. That key can call send, the domain and mailbox routes, webhooks, and the customer MCP tools. It does not add the domain or create mailboxes. Default quotas are 100 credits, 5 sends per minute, and 50 per UTC day.status: "setting_up"has no key. The message is “We're setting up your account. We'll email you when it's ready.” An account already on file is left unchanged. If it can already send, the response is a standard key once. If setup is still in progress, the response issetting_up. Otherwisestatus: "unavailable"and the message is “This request could not be completed.” There is no key. - Store the key in a secret store. It is shown once. Send it as
Authorization: Bearer msn_…orX-Api-Key: msn_…. IfX-Api-Keyis set, it is the only credential checked. Do not commit it.
A standard key gets 403 on admin routes and on list_domain_allowlist, add_domain_allowlist, remove_domain_allowlist, check_domain_allowlist, and operator_remove_domain. Limits on request_access: 5/hour per IP and 3/hour per email. 429 includes retryAfter seconds. Wrong code is 401. Five failures lock it (401). Expired code is 410. Unknown requestId is 404. Completing the same request again is 409. Invalid input is 400.
curl
curl -sS -X POST \
https://mailservicenow.com/api/requestAccess \
-H "Content-Type: application/json" \
-d '{
"contactEmail": "ops@example-domain.com",
"domain": "example-domain.com",
"agentName": "ExampleAgent",
"intendedUse": "Send a notification from an agent"
}'
curl -sS -X POST \
https://mailservicenow.com/api/verifyAccessRequest \
-H "Content-Type: application/json" \
-d '{"requestId":"ar_0123456789abcdef01234567","code":"12345678"}'
MCP JSON-RPC
curl -sS -X POST \
https://mailservicenow.com/api/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "request_access",
"arguments": {
"contactEmail": "ops@example-domain.com",
"domain": "example-domain.com",
"agentName": "ExampleAgent",
"intendedUse": "Send a notification from an agent"
}
}
}'
Then call verify_access_request with requestId and code. Read structuredContent.status (ready or setting_up). The longer copy is in the Markdown guide (How an agent requests access) and llms.txt.
A ready key does not add the domain or create mailboxes. intendedUse does not provision addresses. After the key, follow After you have a standard key (developers quickstart, Domain setup / deliverability). setting_up means the account is still being finished.
Agent onboarding sequence
- Request access (request_access).
- Verify the emailed code (verify_access_request).
- If the result is pending, poll get_request_status (e.g. every 20 minutes). Do NOT resend the same requestId.
- Once the account is ready, get the key with a fresh request_access plus verify_access_request. The key is shown once. "Do NOT resend the same requestId" applies only while the result is pending.
- Add and verify the domain, create a hosted mailbox, and check get_account_status until canSend is true.
- Send, always setting from.
An account can hold one domain. POST /addDomain and MCP add_domain accept the account key. The same normalized name (lowercase, trimmed, trailing dot removed) that is already on the account returns created: false and does not change mailboxes or DNS. A second domain, including a subdomain such as mail.example.com, is 409 one_domain_per_account. After the only domain is removed, one domain can be added again. Accounts that already hold more than one domain keep their mailboxes and DNS. A new add on those accounts is refused.
409 already complete: that request was already finished. Do not retry it. Once the account is ready, get the key with a fresh request_access plus verify_access_request. The key is shown once. "Do NOT resend the same requestId" applies only while the result is pending.
Domain already claimed: the domain belongs to another account. Request a separate account for it only if you own it, or contact support. Each domain needs its own account.
Check if my account is ready
Keep the requestId. If verify returned setting_up, no key exists yet. Mail Service Now emails that address when the account is ready.
Once the account is ready, get the key with a fresh request_access plus verify_access_request. The key is shown once. "Do NOT resend the same requestId" applies only while the result is pending.
Poll get_request_status or POST /getRequestStatus with requestId and email. No API key.
Poll about every 20 minutes. get_request_status allows 10 per hour per IP and 6 per hour per email, counted separately from request_access, so that poll has room for an extra check.
Unknown email, a real email with the wrong requestId, and an unknown requestId all return HTTP 200
{ "ok": true, "status": "pending", "ready": false }.
A match also includes requestId. status is ready, setting_up, pending, or unavailable.
HTTP 429 is { "ok": false, "error": "Rate limit exceeded", "retryAfter": 3600 } whether or not the email is on file.
After verify, call get_account_status or POST /getAccountStatus with that account's own key.
The response has status, accessLevel, domain verification (mx, spf, dkim, dmarc), mailbox ready, and canSend.
When canSend is false, reason is a short next step. The shared agent key, the mail-server sync token, and another account's key are refused.
A hosted address needs a domain you own. DNS: Domain setup / deliverability.
MCP JSON-RPC
Pragmatic JSON-RPC 2.0 over HTTPS (initialize / tools/list / tools/call).
Auth: Authorization: Bearer msn_…
Tools:
send_mail— argsto,subject,text, andfrom. OptionalinReplyToandreferences. Text only. Always setfromto an address on a verified domain you own, for examplesupport@example-domain.com. If it's omitted and the account has exactly one sendable hosted mailbox, that mailbox is used as a fallback; with more than one, omittingfromis an error (400Choose the address to send from.). The shared agent key sends asnoreply@mailservicenow.com.get_credits— returns remaining creditslist_inbox— optionallimit,mailbox→ stored inbound messages andunreadInPage. No credit cost.get_message—idormessageId→ one message, marked read. No credit cost.mark_read—idormessageId, optionalunread→ set the inbound unread flag. No credit cost. Auth failure is HTTP 401 before the tool runs.add_mailbox—domainId,localPart,mode(hosted,forward, orboth),forwardTo(up to 5 addresses) → creates a mailbox. Account setup must be complete. A standard key is accepted. The shared agent key is refused. Forward-only has no IMAP login. The mail server syncs mode changes frommtaMailboxExportabout every 60 seconds.mtaAppliedand its timestamp confirm the change is live. An admin sign-in may add a hosted mailbox onmailservicenow.com.issue_mailbox_credential—address→ issues or resets the IMAP/SMTP app password. The password is returned once and stored as a SHA512-CRYPT hash. A standard key is accepted for a mailbox on that account. The shared agent key is refused. Forward-only is refused.set_mailbox_mode— samemodeandforwardToarguments → updates delivery. Does not delete stored mail. Output includesmode,forwardTo, anddelivery. The same sync confirms the new mode.delete_mailbox—address, ordomainIdandlocalPart→ removes the mailbox record. Routing drops on the next sync, about every 60 seconds. The Maildir is kept. A protected address cannot be deleted (409). The last active mailbox on a verified domain is 409: This is the last mailbox on <domain>. Add another mailbox first, or contact support to remove the domain. There is noforceflag. Unverified domains are not affected. A standard key is accepted. The shared agent key is refused.operator_remove_domain— admin only.uidorownerEmail, plusdomainId. When the domain has any mailbox, also passconfirmset to the domain name. Marks the domainremovedand every mailbox on itremoved. The claim is released when it still names that account. Mail stored for this domain is permanently deleted and cannot be recovered.mailservicenow.comis 409system_domain. A protected domain, and any domain that contains a protected address, is 409. A missing account can still be removed; the audit row setsownerMissing. This is the path for removing a domain's last mailbox. A standard orrequest_accesskey is 403, even for an admin email. The shared agent key and the MTA token are refused.delete_mailboxdoes not release the claim.operator_delete_request— admin only.idorids(at most 200). Deletes those access requests. The account and its key stay as they are. Each id writes one audit row and clears that email's request rate counter. A standard key, the shared agent key, and the MTA token are 403 or 401.request_access— no API key.contactEmail,domain,agentName,intendedUse. Emails a one-time code.verify_access_request— no API key.requestIdandcode. Returns a standard key once, orsetting_up. See Agent onboarding sequence.add_domain— account key. One domain per account. The same normalized name returnscreated: false. A second domain is 409one_domain_per_account.get_request_status— no API key.requestIdandemail. See Check if my account is ready. Poll about every 20 minutes.get_account_status— the account's own key only. Returns status,accessLevel, domain verification, mailbox readiness, andcanSend. Refuses the shared agent key and any other account.
forward relays only and is not a webmail or IMAP login. hosted stores mail. both stores a copy and forwards.
A mailbox cannot forward to itself or into a loop. More than 5 targets is rejected.
Inbound webhooks
POST /webhooks with the agent key registers one HTTPS URL.
A new ingest POSTs inbound.received (id, messageId, from, to, subject, preview, timestamp, mailbox) signed with HMAC-SHA256.
Header X-MSN-Signature: sha256=<hex> over <unix seconds>.<raw body>.
One retry after 750ms. Failures do not fail ingest. No credit.
GET /webhooks reads it. DELETE /webhooks clears it.
Inbox (list / read)
Admins pass a Firebase ID token. Agents pass the same API key as sendMail. Other Firebase users are 403.
Listing does not mark mail read. Fetching one message does.
unreadInPage is only the unread count inside the returned page.
curl -sS \ "https://mailservicenow.com/api/inbox?limit=25" \ -H "Authorization: Bearer msn_<your-key>"
curl -sS \ "https://mailservicenow.com/api/inbox?id=<doc-id>" \ -H "Authorization: Bearer msn_<your-key>"
The human Inbox lists threads with ?view=threads (Inbox unless folder is archive or trash) and opens one with ?thread=<64-hex-id>, which marks that thread read.
POST /inbox with unread or folder marks a thread unread or moves it. Same auth, no credit. list_inbox stays the flat list and does not take folder.
?q= searches subject, from, and body.
Admin drafts are GET/POST/DELETE /drafts with a Firebase ID token. An agent key there is 403. Any other Firebase user is 403.
Threaded sends also store an outbound copy so the next reply can join the thread.
Example: tools/list
curl -sS -X POST \
https://mailservicenow.com/api/mcp \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Example: tools/call send_mail
curl -sS -X POST \
https://mailservicenow.com/api/mcp \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "send_mail",
"arguments": {
"to": "recipient@example.com",
"from": "support@example-domain.com",
"subject": "Hello from MCP",
"text": "Sent via MailServiceNow MCP."
}
}
}'
Example: list_inbox
curl -sS -X POST \
https://mailservicenow.com/api/mcp \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_inbox","arguments":{"limit":25}}}'
Example: get_credits
curl -sS -X POST \
https://mailservicenow.com/api/mcp \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_credits","arguments":{}}}'
Developers · OpenAPI · Download .md · Get started · Sign in