Developers: current docs, quickstart, and the Markdown guide are at /developers/. This page stays available as the legacy agent reference. Download .md · OpenAPI

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 address: always set 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

CallerCredentialHow 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 error Insufficient credits (not 402).
  • Rate limit on send: 5/minute and 50/UTC-day per caller. Same 429 status, error Rate limit exceeded. The agent key shares one bucket.

Endpoints

NameURL
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.

  1. Call request_access or POST /requestAccess with contactEmail, domain, agentName, and intendedUse.
  2. Wait for the email. The code is 8 digits and expires in 24 hours.
  3. Call verify_access_request or POST /verifyAccessRequest with requestId and code.
  4. status: "ready" includes access: "standard" and apiKey once. 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 is setting_up. Otherwise status: "unavailable" and the message is “This request could not be completed.” There is no key.
  5. Store the key in a secret store. It is shown once. Send it as Authorization: Bearer msn_… or X-Api-Key: msn_…. If X-Api-Key is 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

  1. Request access (request_access).
  2. Verify the emailed code (verify_access_request).
  3. If the result is pending, poll get_request_status (e.g. every 20 minutes). Do NOT resend the same requestId.
  4. 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.
  5. Add and verify the domain, create a hosted mailbox, and check get_account_status until canSend is true.
  6. 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 — args to, subject, text, and from. Optional inReplyTo and references. Text only. 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 (400 Choose the address to send from.). The shared agent key sends as noreply@mailservicenow.com.
  • get_credits — returns remaining credits
  • list_inbox — optional limit, mailbox → stored inbound messages and unreadInPage. No credit cost.
  • get_message — id or messageId → one message, marked read. No credit cost.
  • mark_read — id or messageId, optional unread → set the inbound unread flag. No credit cost. Auth failure is HTTP 401 before the tool runs.
  • add_mailbox — domainId, localPart, mode (hosted, forward, or both), 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 from mtaMailboxExport about every 60 seconds. mtaApplied and its timestamp confirm the change is live. An admin sign-in may add a hosted mailbox on mailservicenow.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 — same mode and forwardTo arguments → updates delivery. Does not delete stored mail. Output includes mode, forwardTo, and delivery. The same sync confirms the new mode.
  • delete_mailbox — address, or domainId and localPart → 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 no force flag. Unverified domains are not affected. A standard key is accepted. The shared agent key is refused.
  • operator_remove_domain — admin only. uid or ownerEmail, plus domainId. When the domain has any mailbox, also pass confirm set to the domain name. Marks the domain removed and every mailbox on it removed. The claim is released when it still names that account. Mail stored for this domain is permanently deleted and cannot be recovered. mailservicenow.com is 409 system_domain. A protected domain, and any domain that contains a protected address, is 409. A missing account can still be removed; the audit row sets ownerMissing. This is the path for removing a domain's last mailbox. A standard or request_access key is 403, even for an admin email. The shared agent key and the MTA token are refused. delete_mailbox does not release the claim.
  • operator_delete_request — admin only. id or ids (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. requestId and code. Returns a standard key once, or setting_up. See Agent onboarding sequence.
  • add_domain — account key. One domain per account. The same normalized name returns created: false. A second domain is 409 one_domain_per_account.
  • get_request_status — no API key. requestId and email. 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, and canSend. 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