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

StatusErrorDetails
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
}