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.
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
| Code | HTTP | Meaning |
|---|---|---|
invalid_key | 401 | The key is missing, wrong, expired, revoked or does not have this permission. |
api_disabled / webhooks_disabled | 503 | This part of the API is not available yet. |
not_enabled | 403 | Your workspace has not been enabled for the API. Contact EmailQuanta. |
safety_paused | 403 | Sending paused itself because too many recent emails bounced or drew complaints. Reading still works. EmailQuanta can release it after the cause is fixed. |
safety_hold | 423 | Sending is paused for the workspace by a safety check. |
rate_limited | 429 | Per-minute or per-day limit reached. Wait the number of seconds in the Retry-After header. |
missing_idempotency_key | 422 | Send an Idempotency-Key header (8 to 128 letters, digits and - _ . :). |
idempotency_conflict | 409 | That Idempotency-Key was already used for a different message. |
unknown_sender / sender_not_ready | 422 | The from address is not a sender of this workspace, or its domain is not verified yet. |
invalid_* / missing_body / body_too_large | 422 / 413 | A field is not valid, there is no html or text body, or the message is too large. |
missing_variables / invalid_variables | 422 | A {{placeholder}} has no value, or a variable name or value is not allowed. The email is not sent. |
queue_unavailable | 503 | The email was saved but could not be queued. Retry with the same Idempotency-Key. |
domain_not_ready / no_delivery_path | 409 | The domain is not verified yet, or the workspace has no delivery path set up. |
too_many / too_many_domains / too_many_senders / duplicate | 409 | A limit was reached, or the item already exists. |
not_verified / slow_down | 422 / 429 | Your webhook address did not answer the check correctly, or you checked again too soon. |
not_found | 404 | That 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.
