Skip to content
DEVELOPERS

Send email with the EmailQuanta API

Send one-to-one emails (confirmations, tickets, receipts) from your own website or app, get told what happened to each one, and set up customers' sending domains without opening the dashboard.

The API is for transactional email only. Newsletters and bulk mail go through Campaigns, which has consent, unsubscribe and review built in. API access is switched on per workspace; contact us to ask.

1. Get a key

Sign in, open Integrations / API and create a key. Choose what it may do: Send emails, Read message status, Manage webhooks, Manage domains. The key is shown once; keep it on your server, never in a web page or mobile app. Send it on every request:

Authorization: Bearer eqs_...your key...

All addresses are under https://emailquanta.lealsolution.com/api/v1/developer. Answers are JSON. Errors look like { "error": { "code": "...", "message": "..." } }.

2. Send an email

POST /emails needs a verified sender address of your workspace and a unique Idempotency-Key. If you retry with the same key, the email is never sent twice.

curl -X POST https://emailquanta.lealsolution.com/api/v1/developer/emails \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Idempotency-Key: order-12345-confirmation" \
  -H "Content-Type: application/json" \
  -d '{"from":"tickets@yourdomain.com","to":"buyer@example.com",
       "subject":"Your ticket {{order}}","html":"<p>Hi {{first_name}}</p>",
       "variables":{"order":"A-1042","first_name":"Tom"},"tags":["booking"]}'

The answer is 202 with an id and status: "queued". Fields: from, to, subject, html and/or text, optional reply_to, up to 5 tags, optional variables.

Variables. Write {{name}} in the subject, html or text. Names are letters, digits and _; up to 30 variables of up to 1,000 characters; numbers are fine. Values are made safe inside html. Every placeholder needs a value, otherwise the request is refused and nothing is sent.

Node.js

const res = await fetch("https://emailquanta.lealsolution.com/api/v1/developer/emails", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.EQ_KEY,
    "Idempotency-Key": "order-12345-confirmation",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ from: "tickets@yourdomain.com", to: "buyer@example.com",
    subject: "Your ticket", html: "<p>See you there!</p>" }),
});
const email = await res.json();   // { id, status: "queued", ... }

Python

import os, requests
r = requests.post(
    "https://emailquanta.lealsolution.com/api/v1/developer/emails",
    headers={"Authorization": "Bearer " + os.environ["EQ_KEY"], "Idempotency-Key": "order-12345-confirmation"},
    json={"from": "tickets@yourdomain.com", "to": "buyer@example.com",
          "subject": "Your ticket", "html": "<p>See you there!</p>"},
    timeout=15,
)
email = r.json()

3. Check what happened

GET /emails/{id} shows the status; GET /emails/{id}/events the history; GET /emails your recent emails. Statuses: queued, accepted (the email service took it; this is not proof of inbox delivery), blocked (not sent, with a reason), failed, unknown (uncertain; never retried automatically so nobody gets it twice). Once the receiving side reports back, delivery becomes delivered, delayed, bounced, complained or rejected.

A permanent bounce or a spam complaint adds that address to your do-not-send list automatically, so later emails to it are blocked. If too many recent emails bounce (4% or more) or draw complaints (0.3% or more), sending pauses itself until EmailQuanta releases it.

4. Webhooks: be told instead of asking

With a Manage webhooks key: POST /webhooks with {"url":"https://yourapp.com/eq-events"} returns the address and a signing_secret (shown once). Then POST /webhooks/{id}/verify: EmailQuanta sends a signed webhook.verification message and your address must answer 2xx and include the data.challenge value in its reply. Only verified addresses receive events. Also: GET /webhooks, GET /webhooks/{id}/deliveries, POST /webhooks/{id}/rotate-secret, DELETE /webhooks/{id}. Up to 2 addresses per workspace, public HTTPS only, redirects are not followed.

Events: email.accepted, email.blocked, email.failed, email.unknown, email.delivered, email.delayed, email.bounced, email.complained, email.rejected. They contain the recipient, sender, tags and outcome, never the subject or body.

{"id":"evt_...","type":"email.bounced","api_version":"2026-10","created_at":"...",
 "data":{"email_id":"...","to":"buyer@example.com","from":"tickets@yourdomain.com",
         "tags":["booking"],"status":"accepted","delivery":"bounced",
         "details":{"bounce_type":"Permanent"}}}

Check the signature. Each request has X-EmailQuanta-Event-Id, X-EmailQuanta-Timestamp (unix seconds) and X-EmailQuanta-Signature. The signature is v1= plus the hex HMAC-SHA256, keyed with your signing secret, of timestamp.eventId. followed by the raw body. Reject anything older than 5 minutes, and ignore an event id you already handled (delivery is at least once). Answer 2xx quickly; timeouts and 408/425/429/5xx are retried after 1, 5 and 30 minutes. An address that fails 10 events in a row is turned off.

// Node.js
import crypto from "node:crypto";
export function verify(secret, headers, rawBody) {
  const ts = headers["x-emailquanta-timestamp"], id = headers["x-emailquanta-event-id"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "v1=" + crypto.createHmac("sha256", secret)
    .update(ts + "." + id + ".").update(rawBody).digest("hex");
  const got = String(headers["x-emailquanta-signature"] || "");
  return got.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
# Python
import hmac, hashlib, time
def verify(secret, headers, raw_body: bytes) -> bool:
    ts, event_id = headers["X-EmailQuanta-Timestamp"], headers["X-EmailQuanta-Event-Id"]
    if abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{ts}.{event_id}.".encode() + raw_body, hashlib.sha256)
    return hmac.compare_digest("v1=" + mac.hexdigest(), headers.get("X-EmailQuanta-Signature", ""))

5. Set up a customer's domain

With a Manage domains key: POST /domains with {"domain":"mail.theirsite.com"} returns the DNS records your customer must add and a status. POST /domains/{id}/check re-checks; GET /domains lists. When the status is ready, POST /domains/{id}/senders with {"display_name":"Team","local_part":"team"} adds team@mail.theirsite.com. Up to 5 domains and 20 sender addresses per workspace.

Statuses: verify_dns (add the records, then check again), awaiting_provider_setup (ownership proven; EmailQuanta is finishing verification, check again later), ready. A domain can never send before it is ready.

6. Errors

CodeHTTPMeaning
invalid_key401The key is missing, wrong, expired, revoked or does not have this permission.
api_disabled / webhooks_disabled503This part of the API is not available yet.
not_enabled403Your workspace has not been enabled for the API. Contact EmailQuanta.
safety_paused403Sending paused itself because too many recent emails bounced or drew complaints. Reading still works. EmailQuanta can release it after the cause is fixed.
safety_hold423Sending is paused for the workspace by a safety check.
rate_limited429Per-minute or per-day limit reached. Wait the number of seconds in the Retry-After header.
missing_idempotency_key422Send an Idempotency-Key header (8 to 128 letters, digits and - _ . :).
idempotency_conflict409That Idempotency-Key was already used for a different message.
unknown_sender / sender_not_ready422The from address is not a sender of this workspace, or its domain is not verified yet.
invalid_* / missing_body / body_too_large422 / 413A field is not valid, there is no html or text body, or the message is too large.
missing_variables / invalid_variables422A {{placeholder}} has no value, or a variable name or value is not allowed. The email is not sent.
queue_unavailable503The email was saved but could not be queued. Retry with the same Idempotency-Key.
domain_not_ready / no_delivery_path409The domain is not verified yet, or the workspace has no delivery path set up.
too_many / too_many_domains / too_many_senders / duplicate409A limit was reached, or the item already exists.
not_verified / slow_down422 / 429Your webhook address did not answer the check correctly, or you checked again too soon.
not_found404That item does not exist in your workspace.

7. Rules that always apply

Emails send only from a verified domain and sender, never to an address on your do-not-send list, never while a safety pause is active, and always within your per-minute and per-day limits. There is no option to bypass these. Read the acceptable use policy. Questions: contact us.