JustEmails
PricingSign inStart free trialStart free
API

Three lines of curl. You're sending.

A REST API for transactional email. Same infra as the dashboard — same reputation, same deliverability, same verified domains.

Download OpenAPI 3.1MCP setupGet an API key

Authentication

Every request must carry a bearer token in the Authorization header. Keys start with je_ followed by 64 hex characters, and are shown once at creation — we store only a hash, so keep it somewhere safe. There is no separate test mode: every key sends real mail.

Authorization: Bearer je_3f9a…

Create keys under Settings → API keys in the dashboard, or POST /api-keys. Revoked keys stop working within five seconds. Sending through the API needs a paid plan; accounts in their free trial get 402.

Base URL

https://justemails.app/api/v1

All endpoints below are relative to this base. Every response is JSON. Success responses carry a 2xxstatus and a body with { "data": … }; error responses carry 4xx or 5xx plus a body with { "error": { "code", "message" } }.

Endpoints

Eight endpoints cover send, read, tracking settings and API-key management.

POST/sendSend a transactional email. `from` must be an existing mailbox or alias on a verified domain you own. Returns 202 once queued. Attachments go inline as standard base64 (max 10, 25 MB each, 25 MB total once encoded). Executable and script types are refused with 422; every attachment is scanned for malware before sending, and a detection fails the message and fires the email.failed webhook. Optional `trackOpens` / `trackClicks` (booleans) switch open and click tracking on or off for this send, overriding the account default, which is off — see Open and click tracking below. `data.tracking` in the response says what applied.
Request body
{
  "from":    "hello@yourdomain.com",
  "to":      ["user@example.com"],
  "subject": "Your receipt",
  "html":    "<h1>Thanks!</h1><p>Your receipt is attached.</p>",
  "text":    "Thanks! Your receipt is attached.",
  "replyTo": "support@yourdomain.com",
  "headers": { "X-My-Tag": "receipts" },
  "attachments": [
    {
      "filename":    "receipt-1042.pdf",
      "content":     "JVBERi0xLjQKJ...",
      "contentType": "application/pdf"
    }
  ]
}
Response
{
  "data": {
    "id":          "msg_abc123",
    "status":      "queued",
    "from":        "hello@yourdomain.com",
    "to":          ["user@example.com"],
    "attachments": [
      { "filename": "receipt-1042.pdf", "content_type": "application/pdf", "size_bytes": 48213 }
    ],
    "tracking":    { "opens": false, "clicks": false },
    "created_at":  "2026-04-06T12:00:00Z"
  }
}
GET/messages/:idRetrieve the delivery status and event history of a sent message.
Response
{
  "data": {
    "id":            "msg_abc123",
    "from":          "hello@yourdomain.com",
    "to":            ["user@example.com"],
    "cc":            null,
    "bcc":           null,
    "subject":       "Welcome!",
    "status":        "delivered",
    "status_detail": "Delivered to gmail-smtp-in.l.google.com (TLS): 250 2.0.0 OK",
    "tags":          ["onboarding"],
    "metadata":      null,
    "created_at":    "2026-04-06T12:00:00.000Z",
    "sent_at":       "2026-04-06T12:00:01.402Z",
    "delivered_at":  "2026-04-06T12:00:02.913Z",
    "bounced_at":    null,
    "failed_at":     null,
    "tracking":      { "opens": false, "clicks": false },
    "events": [
      { "event": "queued",    "detail": null, "created_at": "2026-04-06T12:00:00.000Z" },
      { "event": "sent",      "detail": null, "created_at": "2026-04-06T12:00:01.402Z" },
      { "event": "delivered", "detail": "Delivered to gmail-smtp-in.l.google.com (TLS): 250 2.0.0 OK", "created_at": "2026-04-06T12:00:02.913Z" }
    ]
  }
}
GET/messagesList sent messages, newest first. Filters: status, from, to, tag, created_after, created_before (ISO 8601). Page with limit (default 20, max 100); pass pagination.cursor from the previous response back unchanged as ?cursor= until has_more is false.
Response
{
  "data": [
    { "id": "msg_abc123", "status": "delivered", "created_at": "2026-04-06T12:00:00.000Z", ... }
  ],
  "pagination": { "cursor": "2026-04-06T12:00:00.000Z|msg_abc123", "has_more": true }
}
GET/account/api-settingsThe account defaults for open and click tracking on API sends. Both false until you turn them on.
Response
{ "data": { "trackOpens": false, "trackClicks": false } }
PATCH/account/api-settingsChange the tracking defaults. Applies to sends that do not set trackOpens / trackClicks themselves. Owners and admins only, with an unrestricted key; a domain-scoped key gets 403.
Request body
{ "trackOpens": true, "trackClicks": true }
Response
{ "data": { "trackOpens": true, "trackClicks": true } }
POST/api-keysCreate a new API key. Returned once; store it. Optionally scope it to one domain or give it an expiry. Returns 201.
Request body
{
  "name":      "Production backend",
  "domainId":  "5f0c1d2e-…",
  "expiresAt": "2027-01-01T00:00:00Z"
}
Response
{
  "data": {
    "id":        "9b2e6c1a-…",
    "name":      "Production backend",
    "key":       "je_3f9a…(64 hex characters)",
    "keyPrefix": "je_3f9a1c2d",
    "domainId":  null,
    "expiresAt": null,
    "createdAt": "2026-04-06T12:00:00Z"
  }
}
GET/api-keysList all API keys on the account.
Response
{
  "data": [
    {
      "id":         "9b2e6c1a-…",
      "name":       "Production backend",
      "keyPrefix":  "je_3f9a1c2d",
      "lastUsedAt": "2026-04-06T11:50:00Z",
      "expiresAt":  null,
      "revokedAt":  null,
      "createdAt":  "2026-04-06T12:00:00Z"
    }
  ]
}
DELETE/api-keys/:idRevoke an API key. Irreversible — the key stops working immediately.
Response
{ "data": { "id": "9b2e6c1a-…", "revoked": true } }

Code examples

curl
curl -X POST https://justemails.app/api/v1/send \
  -H "Authorization: Bearer $JE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from":    "hello@yourdomain.com",
    "to":      ["user@example.com"],
    "subject": "Welcome!",
    "html":    "<h1>Hello</h1>"
  }'
node
// Node 18+ — no dependencies.
const res = await fetch('https://justemails.app/api/v1/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.JE_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from:    'hello@yourdomain.com',
    to:      ['user@example.com'],
    subject: 'Welcome!',
    html:    '<h1>Hello</h1>',
  }),
});
const { data, error } = await res.json();
python
import os, requests

res = requests.post(
    "https://justemails.app/api/v1/send",
    headers={"Authorization": f"Bearer {os.environ['JE_KEY']}"},
    json={
        "from":    "hello@yourdomain.com",
        "to":      ["user@example.com"],
        "subject": "Welcome!",
        "html":    "<h1>Hello</h1>",
    },
)
body = res.json()  # {"data": {...}} or {"error": {...}}

Webhooks

Register an HTTPS endpoint under Settings → Webhooks and choose its events. Each delivery is a POST of { "event", "timestamp", "data" } with the header X-JustEmails-Signature: sha256=…, the HMAC-SHA256 of the raw body keyed with the webhook's signing secret. A delivery that does not get a 2xx is retried at 30s, 2m, 10m, 1h and 6h.

EventFires when
email.sentAn API message left our servers.
email.deliveredThe recipient's server accepted it; data.status_detail has the SMTP reply.
email.deferredTemporarily refused; we keep retrying.
email.bouncedPermanently refused.
email.complainedA spam complaint (feedback-loop report) was received for it.
email.failedWe could not send it: blocked content, malware, or retries exhausted.
email.openedThe open image in a tracked API message loaded. Only with open tracking on.
email.clickedA tracked link was followed; data.url is the link. Only with click tracking on.
email.receivedOne of your mailboxes received a message. Metadata only, see below.
domain.driftedA verified domain's DNS stopped pointing at us.

email.received

Metadata only — never the body. data.id is the stored email; fetch the body and attachments with GET /emails/{id} (same API key; add ?markRead=false to leave it unread). They are extracted a few seconds after the event fires, so an empty bodyHtml means retry shortly — and is why has_attachments is null in the event. data.to is the address this copy was delivered to (an alias or catch-all address when one was used), data.mailbox where it was stored, and data.folder inbox or spam. Sent only to the webhooks of the account that owns the domain; in an organization, a member's personal mailbox fires nothing.

{
  "event": "email.received",
  "timestamp": "2026-10-01T09:20:11.502Z",
  "data": {
    "id":              "6c1f0e9a-3b7d-4e52-9a1c-2f8e7d6c5b4a",
    "message_id":      "CAF=abc123@mail.gmail.com",
    "mailbox":         "support@yourdomain.com",
    "from":            "Alice Example <alice@example.com>",
    "to":              ["help@yourdomain.com"],
    "subject":         "Order 1042 never arrived",
    "received_at":     "2026-10-01T09:20:11.497Z",
    "size_bytes":      48213,
    "has_attachments": null,
    "folder":          "inbox",
    "timestamp":       "2026-10-01T09:20:11.497Z"
  }
}

Open and click tracking

Off by default, and only ever applied to mail sent through this API — never to mail from your mailboxes (webmail, IMAP/SMTP clients, forwarding). Turn it on for every send with PATCH /account/api-settings or in the dashboard, or per send with trackOpens / trackClicks, which override the default either way.

Only the HTML part changes. Opens add a 1×1 image; clicks rewrite each http(s) link to a signed https://justemails.app/t/c/… redirect that records the click and forwards to your link — and only to the link it was signed for. The plain-text part is sent exactly as written. Add data-je-notrack to an <a> to leave it alone. The rewrite happens before DKIM signing.

Each open or click is an opened / clickedevent on the message (the click's detail is the link) and an email.opened / email.clicked webhook carrying the message's id, from, to, subject, tags and metadata, plus urlfor a click. We do not record the reader's IP address or device. Repeats within a minute count once; at most 50 opens and 50 clicks are recorded per message per hour.

Opens are approximate. Apple Mail Privacy Protection loads images on the reader's behalf, so opens are recorded that never happened; Gmail and other image proxies cache the image, so repeat opens are not seen; clients that block images record nothing. Link scanners can register clicks. With several recipients on one message, an open or click cannot be tied to one of them. You are responsible for any notice or consent your recipients' law requires.

Rate limits

Request rates are counted per client IP over the window shown. Sending limits are counted per account, across the dashboard and the API. Exceeding either returns 429 Too Many Requests with a Retry-After header telling you how long to back off, and an error.code naming the limit.

CategoryLimitApplies to
Send100 / hourPOST /send
Read100 / minuteGET /messages, GET /messages/:id, GET /api-keys
Key management30 / minutePOST /api-keys, DELETE /api-keys/:id
Monthly API sends1,000 / monthIncluded in the base plan; the API add-on raises it. error.code API_QUOTA_EXCEEDED
New accounts20 / dayFor the first 7 days after signup. error.code FRESH_ACCOUNT_CAPPED
Daily sendingper accountDashboard and API sends combined; grows with account standing. error.code RATE_LIMITED
New domainswarm-upA newly verified domain ramps up daily volume. error.code WARM_UP_LIMIT

Error codes

Every error response carries a JSON body with error.code (machine-readable) and error.message (human).

HTTPMeaningWhen
400Bad RequestBody is not valid JSON, or an API-key request failed validation.
401UnauthorizedMissing, invalid, expired or revoked API key.
402Payment RequiredAPI access needs a paid plan. Accounts still in their free trial get this on POST /send.
403ForbiddenKey lacks permission, or the from-domain is not yours, not verified, or the from address is not a mailbox or alias on it.
404Not FoundResource does not exist.
409ConflictIdempotency-Key reused with a different request body.
413Payload Too LargeAn attachment is over 25 MB, or the encoded message exceeds 25 MB.
422Unprocessable EntityRequest failed validation — field errors are in error.details — or an attachment type is refused.
429Too Many RequestsA rate limit or sending limit was hit (see Rate limits). error.code says which; wait for Retry-After.
500Internal Server ErrorServer-side failure. Retry after a short backoff.
Beyond messages

140+ tools exposed via MCP.

Every action in the dashboard — domain add, DKIM rotate, alias create, forward rule, invoice fetch — is callable from any MCP-aware AI. Wire JustEmails into Claude, ChatGPT, Cursor, or any client that speaks the protocol.

Download the tool catalogue (OpenAPI JSON) →