API v1

NSEND Webmail API

A REST API for third-party apps. Authenticate with a personal API token and work with every mailbox on the token owner's account — read, search, organise and send.

Authentication

Every request carries a bearer token in the Authorization header. Create one in NSEND under Settings → API tokens.

# Base URL
https://webmail.nsend.net/api/v1

# Every request
curl https://webmail.nsend.net/api/v1/me \
  -H "Authorization: Bearer nsnd_YOUR_TOKEN_HERE"

A token is as powerful as a password. Depending on its scopes it can read every message in every mailbox on the account and send mail as its owner. Keep it server-side — never ship one in a browser app, mobile binary or public repository. If a token leaks, revoke it in Settings; revocation takes effect on the next request.

The secret is shown once, when you create it. NSEND stores only a hash, so a lost token cannot be recovered — issue a new one. Changing or resetting the account password revokes every token on that account.

Session cookies are deliberately not accepted here. Browsers attach cookies automatically, so a cookie-authenticated API would let any page the user visits drive their mailbox. Requiring an explicit header means a caller must have been given a token on purpose.

Scopes

A token carries only the permissions you tick when creating it. Calling an endpoint outside them returns 403 insufficient_scope.

ScopeGrants
accounts:readList the mailboxes on the account (addresses and display names — never passwords or server settings)
mail:readRead folders, messages, attachments, raw source and search results
mail:writeChange flags, move and delete messages, create/rename/delete folders
mail:sendSend email as the token owner
contacts:readRead the address book
settings:readRead app settings
settings:writeChange app settings

Tokens cannot manage other tokens, add or remove mailboxes, or change the account password — those stay behind an interactive sign-in, so a leaked token can't entrench itself.

Errors

Errors use standard HTTP status codes with a JSON body: { "error": "…", "code": "…" }.

StatusCodeMeaning
400bad_requestMissing or malformed parameter
401no_tokenNo Authorization: Bearer header
401invalid_tokenUnknown, expired or revoked token
403insufficient_scopeToken lacks the required scope
404not_foundNo such account, message or attachment
429—Rate limit exceeded
502send_failedThe upstream SMTP server rejected the message

Requesting an account the token's owner does not own returns 404, not 403 — the API does not confirm whether another tenant's account id exists.

Rate limits

300 requests per minute per token. Buckets are per token rather than per IP, so one busy integration can't exhaust another's budget. Over the limit you get 429; retry after the window resets.

Identity

GET /me

Who this token belongs to and what it may do. Useful to validate a token at setup time.

{
  "id": "5f3c…",
  "email": "you@example.com",
  "scopes": ["accounts:read", "mail:read", "mail:send"]
}

Accounts

GET /accounts accounts:read

Every mailbox linked to the account. Use these ids in the routes below.

{
  "accounts": [
    { "id": "a1b2…", "email": "you@example.com", "displayName": "You" }
  ]
}

Folders

EndpointScopeNotes
GET /accounts/:id/foldersmail:readAll folders with total and unread counts
POST /accounts/:id/foldersmail:writeBody {"name":"Receipts"} → 201
PATCH /accounts/:id/foldersmail:writeBody {"path":"Receipts","name":"Invoices"} — returns the new path
DELETE /accounts/:id/folders?path=mail:writeDeletes the folder and its contents

A folder is addressed either by its system id (inbox, sent, drafts, trash, archive, spam) or by its raw IMAP path. System ids are portable across providers; prefer them where you can.

Messages

IMAP UIDs are unique only within a folder, so every message route takes a folder (defaulting to inbox).

GET /accounts/:id/messages mail:read

Query: folder, limit (1–200, default 50), query (server-side search within the folder).

curl "$BASE/accounts/$ACCT/messages?folder=inbox&limit=20" \
  -H "Authorization: Bearer $TOKEN"

{
  "folder": "inbox",
  "messages": [{
    "uid": 4821,
    "folder": "inbox",
    "from": { "name": "Ada", "email": "ada@example.com" },
    "to": [{ "name": "", "email": "you@example.com" }],
    "subject": "Invoice #4021",
    "date": "2026-07-21T14:03:11.000Z",
    "unread": true, "starred": false,
    "hasAttachments": true,
    "preview": "Attached is the invoice…",
    "messageId": "<abc@example.com>"
  }]
}

GET /accounts/:id/messages/:uid mail:read

Full message. html is sanitised for safe rendering; attachments are listed as metadata only, so the response stays small regardless of payload size.

{
  "uid": 4821, "folder": "inbox",
  "html": "<p>Attached is the invoice…</p>",
  "attachments": [
    { "index": 0, "filename": "invoice.pdf",
      "size": 48210, "contentType": "application/pdf", "isImage": false }
  ]
}
EndpointScopeNotes
GET /messages/:uid/rawmail:readOriginal RFC 822 source as message/rfc822
GET /messages/:uid/attachments/:indexmail:readRaw attachment bytes with its own content type
PATCH /messages/:uidmail:writeBody {"seen":true,"flagged":false} — either or both
DELETE /messages/:uidmail:writeMoves to Trash (or expunges if already there)
POST /messages/movemail:writeBody {"folder":"inbox","uids":[1,2],"dest":"archive"}
POST /messages/flagsmail:writeBody {"folder":"inbox","uids":[1,2],"seen":true} — bulk
GET /accounts/:id/search?q=mail:readSearches every folder, not just one

Sending

POST /accounts/:id/messages/send mail:send

Sends over the mailbox's own SMTP server and appends a copy to Sent. Requires to plus at least one of text / html.

curl -X POST "$BASE/accounts/$ACCT/messages/send" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["ada@example.com"],
    "cc": [],
    "subject": "Re: Invoice #4021",
    "html": "<p>Paid — thanks!</p>",
    "inReplyTo": "<abc@example.com>",
    "trackReceipt": false
  }'

{ "ok": true, "messageId": "<xyz@nsend>", "appendedToSent": true }

Attachments go in attachments as {"filename","contentType","contentBase64"}. Set trackReceipt: true to embed a read-receipt pixel. An SMTP rejection surfaces as 502 send_failed.

Contacts

GET /accounts/:id/contacts contacts:read

The address book for a mailbox — manually added entries plus addresses harvested from history. Returns { "contacts": [{ "email", "name", "manual" }] }.

Settings

EndpointScopeNotes
GET /settingssettings:readThe account's app settings object
PATCH /settingssettings:writeShallow merge — send only the keys you're changing

Real-time stream

GET /stream mail:read

A Server-Sent Events stream that pushes new mail as it arrives, so you don't have to poll. Holding the connection open keeps IMAP IDLE watchers running for the account.

# events
event: ready       # connection established
data: {}

event: mail
data: {"accountId":"a1b2…","count":1,"from":"Ada","subject":"Invoice #4021"}

# a ": keep-alive" comment arrives every 25s

The event announces that mail arrived, not its contents — fetch the message with GET /messages when you receive one. EventSource in the browser cannot set headers, so use an SSE client that can send Authorization (and remember a token does not belong in a browser anyway).

Versioning Everything above lives under /api/v1. Additive changes (new endpoints, new response fields) can land in v1; anything that would break an integration ships as /api/v2.