Auth
mail.mailservicenow.com. You call HTTPS only.
Always set from to an address on a verified domain you own, for example support@example-domain.com.
Examples use msn_<your-key>. Never commit a live key.
export MSN_API_BASE=https://mailservicenow.com/api
export MSN_AGENT_API_KEY='msn_<your-key>'
| Caller | Header | Routes |
|---|---|---|
| Agent or product backend | Authorization: Bearer msn_<your-key> or X-Api-Key: msn_<your-key> |
sendMail, inbox, mcp |
Confirm the server before the first send. GET /mcp needs no key and lists
send_mail, get_credits, list_inbox, get_message, mark_read.
Register an inbound webhook with POST /webhooks (see Developers).
curl -sS "$MSN_API_BASE/mcp"
Send
Plain text. to, subject, and text are required. 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.
REST
curl -sS -X POST \
"$MSN_API_BASE/sendMail" \
-H "Authorization: Bearer $MSN_AGENT_API_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."
}'
{
"ok": true,
"messageId": "<id@mail.mailservicenow.com>",
"creditsRemaining": 99
}
MCP
curl -sS -X POST "$MSN_API_BASE/mcp" \
-H "Authorization: Bearer $MSN_AGENT_API_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 an agent",
"text": "Sent via MailServiceNow MCP."
}
}
}'
REST: require HTTP 200 and ok: true, then store messageId.
MCP: require HTTP 200 and result.structuredContent.ok === true.
result.isError: true is a failed tool call even when the HTTP status is 200.
Auth failures stay HTTP 401.
Credits
- One agent send costs 1 credit. The default key starts at 100. People signed in to the web app are not charged and do not get
creditsRemaining. - Read the balance before a batch.
get_creditsand inbox reads are free. - The credit is taken after validation and before SMTP. A 400 or a rate-limit 429 does not spend one. An MTA failure does not refund it.
- Zero credits:
429Insufficient credits. Stop and ask Mail Service Now. Do not look for HTTP 402. GET /creditsshows remaining credits and the key’s last four characters. It is free. Signed-in Settings shows the same balance.503Agent key disabledmeans that key is frozen. Do not spend a retry budget on it, and do not fail over to another ESP.503Sending temporarily disabledpauses every sender.- Burst or daily cap:
429Rate limit exceeded. Limits are 5 per minute and 50 per UTC day for the shared agent bucketagent:default.
curl -sS -X POST "$MSN_API_BASE/mcp" \
-H "Authorization: Bearer $MSN_AGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "get_credits", "arguments": {} }
}'
Tool result shape: { "ok": true, "remaining": 100 } inside structuredContent.
Optional inbox
Skip this step when the product only sends. The shared inbox@mailservicenow.com mailbox stays with Mail Service Now. It is not a customer mailbox. Do not tell a customer to write there.
- Always set
fromto an address on a verified domain you own, for examplesupport@example-domain.com. Mail tonoreply@mailservicenow.comis not a customer mailbox. - List, then get. List rows have no body. Getting a message marks it read.
mailbox, when set, must be an@mailservicenow.comaddress. This is not a reader for a hosted mailbox on a customer domain. That mailbox opens in the web app.- A follow-up you send is a new message. Always set
fromto an address on a verified domain you own, for examplesupport@example-domain.com. Pass optionalinReplyToandreferencesto keep it on the thread.list_inboxstays a flat list.
curl -sS "$MSN_API_BASE/inbox?limit=25" \
-H "Authorization: Bearer $MSN_AGENT_API_KEY"
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "list_inbox",
"arguments": { "limit": 25, "mailbox": "inbox@mailservicenow.com" }
}
}
Errors
| What you see | Do |
|---|---|
401 Invalid API key or missing credential |
Fix the header. Do not retry in a loop. |
400 |
Fix to, subject, or text. Max 5 recipients, max 100KB. |
429 Rate limit exceeded |
Back off. Same string on MCP via structuredContent.status. |
429 Insufficient credits |
Stop the batch. |
503 Sending temporarily disabled |
Stop. Do not fail over to another mail provider. |
404 Message not found |
The inbox id is wrong. Listing again is free. |
Contract: Developers, OpenAPI, Download .md.
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 at
contactEmail. 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. Continue with Domain and 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_…, the same way as any othermsn_…key. IfX-Api-Keyis set, it is the only credential checked. Do not commit the key.
A standard key gets 403 on admin routes and on the admin MCP tools. request_access allows 5 requests per hour per IP and 3 per hour per email. 429 includes retryAfter seconds. Wrong code is 401. Five failures lock the code (401). An expired code is 410. An unknown requestId is 404. Completing the same request again is 409. Invalid input is 400.
curl
curl -sS -X POST "$MSN_API_BASE/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 "$MSN_API_BASE/verifyAccessRequest" \
-H "Content-Type: application/json" \
-d '{"requestId":"ar_0123456789abcdef01234567","code":"12345678"}'
MCP JSON-RPC
curl -sS -X POST "$MSN_API_BASE/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 is ready or setting_up. Full contract: Markdown guide (How an agent requests access), llms.txt, OpenAPI.
After you have a standard key: set up your domain and mailboxes
DNS for each record, including DMARC, is in Domain setup / deliverability.
Use your hostname in place of example.com. A ready key does not add it. intendedUse does not create mailboxes. Field notes: Markdown guide.
readywithmsn_…means you can send and call domain routes. It does not add the domain or create mailboxes.setting_uphas no key. Domain calls then return 403Account setup is not complete yet.POST /addDomainwith{"domain":"example.com"}, or Add domain in the web app. A first response ofcreated: trueandstatus: "failed"means DNS is not in place yet. ReaddnsInstructions.records.- Publish MX
mail.mailservicenow.com(priority 10), one SPF TXT copied from the dashboard (SPF), and DKIM atmail._domainkeyfrom the response. Moving MX off Cloudflare Email Routing, or any previous host, stops mail that host delivers today. Cut over when you mean to. POST /verifyDomainwith{"domainId":"example.com"}, or Check DNS in the app.verified: truesetsstatustoverified.POST /addMailboxfor eachlocalPart. The standard key works on REST and on MCPadd_mailbox. A Firebase ID token also works. The shared agent key is refused withFirebase sign-in required. MCPadd_domainadds the one domain.- Wait about 60 seconds (
mtaApplied). IMAP, SMTP, and webmail need a verified domain and modehostedorboth. - Optional: webhooks, a proof send, credits. Send before that mailbox exists returns 403
Add a fully hosted mailbox on a verified domain before sending.
An admin pending row with storedStatus: "code_sent" (including an expired code) has decidable: false. It is not a mailbox hold. setting_up means the account is still being finished. After ready, do this list.
curl -sS -X POST "$MSN_API_BASE/addDomain" \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{"domain":"example.com"}'
curl -sS -X POST "$MSN_API_BASE/verifyDomain" \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{"domainId":"example.com"}'
curl -sS -X POST "$MSN_API_BASE/addMailbox" \
-H "Authorization: Bearer msn_<your-key>" \
-H "Content-Type: application/json" \
-d '{"domainId":"example.com","localPart":"orders","mode":"hosted"}'