Emails API
Send an email
Queue a transactional email for immediate or scheduled delivery. Supports raw HTML or saved templates, multiple recipients, plain-text fallback, and idempotent sends.
POST
/v1/emails/send
Queue a single email for delivery. Returns an email ID you can use to check status or cancel a scheduled send.
🔑 API key —Authorization: Bearer qle_…
⚡ Idempotent
📅 Schedulable
📦 Up to 50 recipients
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | string | required | Verified sender address. Either "email@domain.com" or "Name <email@domain.com>". The domain must be verified in your account. |
| to | string | string[] | required | Recipient address(es). A single email string or an array of up to 50 addresses. |
| subject | string | required | Email subject line. Max 998 characters. Required in HTML mode. |
| html | string | required | HTML body of the email. Cannot be used together with template. |
| text | string | optional | Plain-text fallback. Auto-generated from the HTML when omitted. |
| replyTo | string | optional | Reply-to address. Does not need to be a verified domain. |
| scheduledAt | string | optional | ISO 8601 datetime for deferred delivery. Must be in the future. Omit to send immediately. |
| metadata | object | optional | Arbitrary key-value data stored with the email record. Useful for linking sends to your internal IDs. |
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | string | required | Verified sender address. Either "email@domain.com" or "Name <email@domain.com>". |
| to | string | string[] | required | Recipient address(es). A single email string or an array of up to 50 addresses. |
| template | string | required | Slug of a saved template (e.g. "welcome-email"). Cannot be used together with html. |
| variables | object | optional | Handlebars context passed to the template. Must include every {{variable}} referenced in the template body and subject — missing variables cause a 422. |
| subject | string | optional | Overrides the template's subject when provided. The template's own subject is used when omitted. |
| replyTo | string | optional | Reply-to address. |
| scheduledAt | string | optional | ISO 8601 datetime for deferred delivery. Must be in the future. |
| metadata | object | optional | Arbitrary key-value data stored with the email record. |
Idempotency. Pass an
Idempotency-Key: <unique-string> header to make the request safe to retry. Duplicate requests with the same key within 24 hours return the original response with an Idempotency-Replay: true header — no second email is sent.
Errors
| Status | Error | Details |
|---|---|---|
| 400 | Sending domain not verified | The domain in your from address has not been verified. Body includes domain. |
| 400 | scheduledAt must be a future datetime | The supplied scheduledAt is in the past. |
| 402 | Monthly limit reached | Monthly quota exhausted. Body includes limit, used, plan. Upgrade or wait for the next billing cycle. |
| 404 | Template not found | No template with the given slug exists in your account. Template mode only. |
| 422 | Either html or template is required | Request body must include exactly one of html or template. |
| 422 | subject is required when sending raw html | subject was omitted in HTML mode. |
| 422 | Template render failed: … | Handlebars strict mode — a {{variable}} in the template has no matching key in variables. Template mode only. |
| 429 | Daily sending limit reached | Daily cap hit (≈ monthly limit ÷ 30). Body includes dailyLimit, dailySent, resetsAt ("midnight UTC"), plan. |
| 503 | Sending temporarily paused — account under review | Account flagged for high bounce or complaint rate. Body includes reason: "bounce" or "complaint". Contact support. |
Examples
curl -X POST https://api.quolle.com/v1/emails/send \
-H "Authorization: Bearer qle_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme ",
"to": "customer@example.com",
"subject": "Your order is confirmed",
"html": "<h1>Order confirmed</h1><p>Thanks for your purchase!</p>"
}'
const res = await fetch("https://api.quolle.com/v1/emails/send", {
method: "POST",
headers: {
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Acme <hello@mail.yourdomain.com>",
to: "customer@example.com",
subject: "Your order is confirmed",
html: "<h1>Order confirmed</h1><p>Thanks for your purchase!</p>",
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error);
}
const { id } = await res.json();
console.log("Queued:", id);
import requests
response = requests.post(
"https://api.quolle.com/v1/emails/send",
headers={
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
json={
"from": "Acme <hello@mail.yourdomain.com>",
"to": "customer@example.com",
"subject": "Your order is confirmed",
"html": "<h1>Order confirmed</h1><p>Thanks for your purchase!</p>",
},
timeout=15,
)
response.raise_for_status()
data = response.json()
print("Queued:", data["id"])
curl -X POST https://api.quolle.com/v1/emails/send \
-H "Authorization: Bearer qle_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@mail.yourdomain.com>",
"to": "customer@example.com",
"template": "order-confirmation",
"variables": {
"firstName": "Amara",
"orderNumber": "ORD-8821",
"totalNaira": "12,500"
}
}'
const res = await fetch("https://api.quolle.com/v1/emails/send", {
method: "POST",
headers: {
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Acme <hello@mail.yourdomain.com>",
to: "customer@example.com",
template: "order-confirmation",
variables: {
firstName: "Amara",
orderNumber: "ORD-8821",
totalNaira: "12,500",
},
}),
});
const { id } = await res.json();
console.log("Queued:", id);
import requests
response = requests.post(
"https://api.quolle.com/v1/emails/send",
headers={
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
json={
"from": "Acme <hello@mail.yourdomain.com>",
"to": "customer@example.com",
"template": "order-confirmation",
"variables": {
"firstName": "Amara",
"orderNumber": "ORD-8821",
"totalNaira": "12,500",
},
},
timeout=15,
)
response.raise_for_status()
print("Queued:", response.json()["id"])
curl -X POST https://api.quolle.com/v1/emails/send \
-H "Authorization: Bearer qle_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@mail.yourdomain.com>",
"to": "customer@example.com",
"subject": "Your trial is ending soon",
"html": "<p>Your 14-day trial ends tomorrow.</p>",
"scheduledAt": "2026-07-10T09:00:00.000Z"
}'
const res = await fetch("https://api.quolle.com/v1/emails/send", {
method: "POST",
headers: {
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Acme <hello@mail.yourdomain.com>",
to: "customer@example.com",
subject: "Your trial is ending soon",
html: "<p>Your 14-day trial ends tomorrow.</p>",
scheduledAt: "2026-07-10T09:00:00.000Z",
}),
});
const { id, status, scheduledAt } = await res.json();
console.log(`Scheduled ${id} for ${scheduledAt}`);
import requests
response = requests.post(
"https://api.quolle.com/v1/emails/send",
headers={
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
json={
"from": "Acme <hello@mail.yourdomain.com>",
"to": "customer@example.com",
"subject": "Your trial is ending soon",
"html": "<p>Your 14-day trial ends tomorrow.</p>",
"scheduledAt": "2026-07-10T09:00:00.000Z",
},
timeout=15,
)
data = response.json()
print(f"Scheduled {data['id']} for {data['scheduledAt']}")
Response
200 OK
Immediate send
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"message": "Email queued successfully"
}
200 OK
Scheduled send
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "scheduled",
"scheduledAt": "2026-07-10T09:00:00.000Z",
"message": "Email scheduled successfully"
}
Error responses
402 — Monthly limit reached
{
"error": "Monthly limit reached",
"limit": 3000,
"used": 3000,
"plan": "Starter"
}
429 — Daily limit reached
{
"error": "Daily sending limit reached",
"dailyLimit": 100,
"dailySent": 100,
"resetsAt": "midnight UTC",
"plan": "Starter"
}
503 — Account paused
{
"error": "Sending temporarily paused — account under review",
"reason": "bounce"
}
Checking delivery status. Retrieve an email by ID using
GET /v1/emails/:id (API key required) to check its current status
programmatically. For real-time notifications, use
webhooks — subscribe to email.delivered,
email.bounced, and other events. View delivery history in the
dashboard
under Logs.
POST
/v1/emails/batch
Send up to 100 emails in a single request. The entire batch is checked against your monthly and daily limits before any email is queued — if any limit would be exceeded, the whole batch is rejected.
🔑 API key —Authorization: Bearer qle_…
⚡ Idempotent
📦 Up to 100 emails per batch
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| emails | object[] | required | Array of 1–100 email objects. Each object accepts the same fields as POST /v1/emails/send. |
Errors
| Status | Error | Details |
|---|---|---|
| 402 | This batch of N would exceed your monthly limit… | Body includes canSend — the number of emails you can still send this month. |
| 429 | This batch would exceed your daily limit of N… | Body includes canSend — the number you can still send today. |
All-or-nothing. If the batch would exceed a limit, none of the emails are queued. Use the
canSend value in the error response to split and retry a smaller batch.
Example
curl -X POST https://api.quolle.com/v1/emails/batch \
-H "Authorization: Bearer qle_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{
"from": "hello@mail.yourdomain.com",
"to": "alice@example.com",
"subject": "Welcome, Alice!",
"html": "<p>Thanks for joining.</p>"
},
{
"from": "hello@mail.yourdomain.com",
"to": "bob@example.com",
"subject": "Welcome, Bob!",
"html": "<p>Thanks for joining.</p>"
}
]
}'
const users = [
{ email: "alice@example.com", name: "Alice" },
{ email: "bob@example.com", name: "Bob" },
];
const res = await fetch("https://api.quolle.com/v1/emails/batch", {
method: "POST",
headers: {
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
emails: users.map(u => ({
from: "hello@mail.yourdomain.com",
to: u.email,
subject: `Welcome, ${u.name}!`,
html: `<p>Hi ${u.name}, thanks for joining.</p>`,
})),
}),
});
const { queued, ids } = await res.json();
console.log(`Queued ${queued} emails`, ids);
import requests
users = [
{"email": "alice@example.com", "name": "Alice"},
{"email": "bob@example.com", "name": "Bob"},
]
response = requests.post(
"https://api.quolle.com/v1/emails/batch",
headers={
"Authorization": "Bearer qle_your_api_key",
"Content-Type": "application/json",
},
json={
"emails": [
{
"from": "hello@mail.yourdomain.com",
"to": u["email"],
"subject": f"Welcome, {u['name']}!",
"html": f"<p>Hi {u['name']}, thanks for joining.</p>",
}
for u in users
]
},
timeout=30,
)
response.raise_for_status()
data = response.json()
print(f"Queued {data['queued']} emails:", data["ids"])
Response
200 OK
{
"queued": 2,
"ids": [
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"b2c3d4e5-f6a7-8901-bcde-f01234567891"
]
}
402 — Batch would exceed monthly limit
{
"error": "This batch of 50 would exceed your monthly limit of 3000. You can send 12 more this month.",
"canSend": 12
}
429 — Batch would exceed daily limit
{
"error": "This batch would exceed your daily limit of 100. You can send 20 more today.",
"canSend": 20
}