# Open Mail API documentation


## Connect your app in four steps


Open Mail sends emails through your own SMTP account. You will need a customer account, SMTP credentials and a backend that can make HTTP requests.


API base URL`https://openmail.vishnudeepak.com/api`

Follow steps 1–4 below. After your first email works, add attachments or scheduling if needed.


## 1. Connect your SMTP account


- Log in to Open Mail and open SMTP accounts.
- Click Add SMTP. Enter your provider’s hostname, username, password and authorized sender email.
- Choose STARTTLS / 587 or TLS / 465, then save.
- Click Test connection. If it fails, check your provider’s credentials.
- Click Copy ID. Save this value as `OPEN_MAIL_SMTP_ID` on your server.

A successful connection test verifies SMTP access. Your provider must also authorize the sender address before it can accept email.


## 2. Create your API key


- Open API keys and click Create key.
- Give it a name, such as your website’s name.
- Copy the key now; it is shown only once.
- Save it as `OPEN_MAIL_API_KEY` in your backend’s environment.


Required header

```
Authorization: Bearer YOUR_API_KEY
```


Keep keys in server environment variables. Do not put them in browser JavaScript, URLs, repositories or mobile app bundles. Your key can access only your own emails and attachments.


SMTP credentials, connection tests and API-key management use the dashboard login session. `GET /smtp` does not accept API keys; copy the SMTP ID in the dashboard instead.


Regenerating a key invalidates its previous value immediately. Revoking a key blocks subsequent requests. Blocking a customer also pauses their queued sends; an already-running SMTP send may finish.


## 3. Send your first email


Run this command from your terminal. Replace `YOUR_API_KEY`, `YOUR_SMTP_ID` and the recipient address with your own values.


Terminal · cURL

```
curl 'https://openmail.vishnudeepak.com/api/emails' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: welcome-user-123' \
  -d '{
    "smtp_id": "YOUR_SMTP_ID",
    "recipients": ["customer@example.com"],
    "subject": "Welcome",
    "body": "Thanks for signing up.",
    "from_name": "My App"
  }'
```


Expected response · HTTP 202

```
{
  "id": "example-email-id",
  "status": "queued"
}
```


Save the returned `id` for step 4. `queued` means Open Mail accepted the request and will attempt delivery.


### Request fields and HTML email


`POST /emails` requires JSON, a bearer token and an `Idempotency-Key` header.


| Field | Required | Value | 
| --- | --- | --- |
| smtp_id | Yes | ID of an active SMTP account you own. | 
| recipients | Yes | Array of 1–50 email addresses. Comma-separated string also accepted. | 
| subject | Yes | Non-empty string, up to 998 characters; no line breaks. | 
| body | Yes | Non-empty string, up to 50,000 characters. | 
| body_format | No | text (default) or html. | 
| from_name | No | Sender display name; defaults to your account name, capped at 100 characters. | 
| attachment_ids | No | Array of uploaded file IDs, up to 10 files and 10 MB combined. | 
| scheduled_at | No | ISO timestamp with timezone; within one year. | 

The sender email address comes from your SMTP account; there is no per-request sender-address override. CC, BCC and delivery webhooks are not currently supported.


HTML message · JSON payload

```
{
  "smtp_id": "YOUR_SMTP_ID",
  "recipients": [
    "customer@example.com"
  ],
  "subject": "Welcome",
  "body_format": "html",
  "body": "<h1>Welcome</h1><p>Thanks for joining.</p>"
}
```


## 4. Check your email status


Replace `EMAIL_ID` with the ID returned in step 3, and use the same API key.


| Method | Path | Operation | Success | 
| --- | --- | --- | --- |
| POST | /emails | Queue an email | 202 | 
| GET | /emails | Paginated emails, newest first | 200 | 
| GET | /emails/:id | Read one email record | 200 | 
| DELETE | /emails/:id | Cancel a queued email | 200 | 
| POST | /attachments | Upload a private file | 201 | 
| GET | /attachments/:id | Download a private file | 200 | 
| DELETE | /attachments/:id | Delete a file not used by pending mail | 200 | 


cURL · check a message

```
curl 'https://openmail.vishnudeepak.com/api/emails/EMAIL_ID' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```


200 OK · abbreviated example

```
{
  "id": "example-email-id",
  "status": "sent",
  "attempts": 1,
  "error": null,
  "provider_id": "<example-message-id>",
  "created_at": 1790000000000,
  "sent_at": 1790000005000
}
```


Email records include `source`: `api` or `dashboard`. Records created before source tracking show `unknown`. Timestamps in records are Unix milliseconds. `recipients` and `attachment_ids` are currently JSON-encoded strings in read responses; parse them if needed. Listing returns an array with 20 records per page by default. Use `?page=2&per_page=20` to request another page (maximum 20 records). Pagination metadata is in `X-Total-Count`, `X-Page`, `X-Page-Size` and `X-Total-Pages` response headers. Search with `q`; filter with `status=pending`, `sent`, `attention`, `cancelled` or an individual delivery status. Filters apply before pagination.


- `queued`: waiting for the scheduler, retry or sending capacity.
- `sending`: the consumer has claimed the email.
- `sent`: SMTP server accepted it; inbox delivery is not confirmed.
- `failed`: permanent error or exhausted retries. Read `error`.
- `partial`: some recipients were rejected; investigate before retrying.
- `uncertain`: delivery was interrupted and acceptance is unknown. Check your SMTP provider before resending.
- `cancelled`: stopped while queued.


cURL · cancel queued mail

```
curl -X DELETE 'https://openmail.vishnudeepak.com/api/emails/EMAIL_ID' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```


Cancellation returns `{"ok":true}`. A message already sending or completed returns 409. Poll status at a reasonable interval, such as every 10–30 seconds, and stop at a final status.


## Use your programming language


Choose an example for your backend. Set `OPEN_MAIL_BASE_URL` to `https://openmail.vishnudeepak.com/api`, plus the SMTP ID and API key from steps 1–2.


### Node.js


Node.js · server-side

```
// Node.js 22+. Run on your server, not in browser JavaScript.
const base = process.env.OPEN_MAIL_BASE_URL; // https://openmail.vishnudeepak.com/api
const apiKey = process.env.OPEN_MAIL_API_KEY;
const smtpId = process.env.OPEN_MAIL_SMTP_ID;

async function sendWelcome(customerEmail, eventId) {
  const response = await fetch(base + '/emails', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + apiKey,
      'Content-Type': 'application/json',
      'Idempotency-Key': eventId // Persist and reuse for this event.
    },
    body: JSON.stringify({
      smtp_id: smtpId,
      recipients: [customerEmail],
      subject: 'Welcome',
      body: 'Thanks for signing up.',
      from_name: 'My App'
    })
  });
  const result = await response.json();
  if (!response.ok) {
    throw new Error(response.status + ': ' + result.error);
  }
  return result; // { id, status: 'queued' }
}

console.log(await sendWelcome('customer@example.com', 'welcome-user-123'));
```


### Python


Python · standard library

```
# Python 3; standard library only. Run on your server.
import json, os
from urllib.request import Request, urlopen
from urllib.error import HTTPError

payload = {
    'smtp_id': os.environ['OPEN_MAIL_SMTP_ID'],
    'recipients': ['customer@example.com'],
    'subject': 'Welcome',
    'body': 'Thanks for signing up.',
    'from_name': 'My App'
}
request = Request(
    os.environ['OPEN_MAIL_BASE_URL'] + '/emails',
    data=json.dumps(payload).encode('utf-8'),
    headers={
        'Authorization': 'Bearer ' + os.environ['OPEN_MAIL_API_KEY'],
        'Content-Type': 'application/json',
        'Idempotency-Key': 'welcome-user-123'
    },
    method='POST'
)
try:
    with urlopen(request, timeout=30) as response:
        print(json.load(response))
except HTTPError as error:
    print(error.code, error.read().decode('utf-8'))
```


## Private attachments


Upload the raw file first, then include its returned ID in your email request. Do not send multipart forms or Base64 JSON.


cURL · upload a file

```
curl 'https://openmail.vishnudeepak.com/api/attachments' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/pdf' \
  -H 'X-Filename: invoice.pdf' \
  --data-binary @invoice.pdf
```


`Content-Length` is required; cURL supplies it for a local file. Percent-encode `X-Filename` when it contains non-ASCII or special characters. Maximum upload: 10,000,000 bytes.


201 Created · example response

```
{
  "id": "example-file-id",
  "filename": "invoice.pdf",
  "size": 12345
}
```


Send with an attachment · JSON payload

```
{
  "smtp_id": "YOUR_SMTP_ID",
  "recipients": [
    "customer@example.com"
  ],
  "subject": "Invoice",
  "body": "Your invoice is attached.",
  "attachment_ids": [
    "example-file-id"
  ]
}
```


`GET /attachments/:id` returns the file bytes. `DELETE /attachments/:id` returns `{"ok":true}`. Both require a bearer token. Deleting a file referenced by queued or sending mail returns 409.


Files remain private. Cleanup removes files older than 15 days when no queued or sending email references them. SMTP providers may impose lower message limits; email encoding increases attachment size by roughly one third.


## Scheduling


Optional fields in POST /emails

```
{
  "scheduled_at": "2027-01-01T09:00:00Z"
}
```


Use an explicit timezone, such as `Z` for UTC or `+05:30`. Choose a future time within one year. Due messages are picked up approximately every minute; provider throttling and queue load can delay sending. Omit this field to queue immediately.


## Errors & retries


Error response format

```
{
  "error": "Customer rate limit reached. Retry next minute."
}
```


| HTTP | Meaning / action | 
| --- | --- |
| 400 | Invalid fields, missing idempotency key, inactive SMTP account or invalid attachment. Correct the request. | 
| 401 | Missing, invalid or revoked key; account may be blocked. Check credentials. | 
| 403 | Operation requires a dashboard session, or browser origin was rejected. | 
| 404 | Record is missing, expired or belongs to another customer. | 
| 409 | Idempotency conflict, non-queued cancellation or file in use. | 
| 413 | JSON or attachment is too large. | 
| 415 | JSON endpoint received the wrong Content-Type. | 
| 429 | Customer submission limit reached. Retry after the minute window resets, with backoff. | 
| 500 | Server error. Retry the same request with the same idempotency key. | 

Use a unique idempotency key for each logical email (maximum 128 characters). If a request times out or fails, reuse the same key and unchanged JSON payload. Repeating it returns the existing email ID with HTTP 200; changing the payload returns 409. Preserve the original serialization and property order when retrying.


Do not create a new key for a network retry. SMTP delivery itself is not exactly once. The worker attempts transient failures up to three times; partial or uncertain outcomes require review.


## Limits


- 10 MB (10,000,000 bytes) total attachments per email; maximum 10 files.
- 100,000 bytes per JSON request, including UTF-8 encoding and attachments metadata.
- Per-customer submission and send-attempt limits, configured by the site owner.
- One active SMTP send per account. Sender address must be allowed by your provider.
- Only TLS port 465 and STARTTLS port 587. Port 25 is unsupported.
- Use this API from your backend. Cross-origin browser writes are rejected.

### Reply-To

Set `reply_to` in `POST /emails` to one valid email address, such as `support@example.com`. This sets the SMTP Reply-To header; it does not change the authorized sender. The web composer also has an optional Reply-To field.
