Errors

Every error the API returns has a consistent structure and a meaningful HTTP status code. Check the status code first, then read the error field.

Error shape

Most errors follow this envelope:

{
  "error": "Human-readable message"
}

Many errors include additional fields alongside error — each status section below shows the exact shape. Validation errors (400) use a different structure from Zod — see below.

Always branch on the HTTP status code, not the error string. Error messages are human-readable descriptions that may be updated; status codes are stable contracts.
400 Bad Request

Returned for two distinct reasons: schema validation failures (wrong or missing fields) and domain-level validation (domain not verified, bad scheduledAt, etc.). The shapes are different.

Schema validation — Zod error

When a required field is missing or a field has the wrong type, the validator returns the raw Zod result. Each item in issues names the field via path and the problem via message:

// HTTP 400 — missing required field
{
  "success": false,
  "error": {
    "issues": [
      {
        "code": "invalid_type",
        "expected": "string",
        "received": "undefined",
        "path": ["from"],
        "message": "Required"
      }
    ],
    "name": "ZodError"
  }
}

Domain not verified

// HTTP 400
{
  "error": "Sending domain not verified",
  "domain": "mail.yourdomain.com"
}

Invalid from address

// HTTP 400
{
  "error": "Invalid from address",
  "domain": ""
}

Too many recipients

// HTTP 400
{
  "error": "Too many recipients: 75. Maximum is 50.",
  "count": 75,
  "max": 50
}

Invalid or past scheduledAt

// HTTP 400 — not a valid datetime
{ "error": "Invalid scheduledAt datetime" }

// HTTP 400 — in the past
{ "error": "scheduledAt must be a future datetime" }
How to handle: For Zod errors, surface issues[].message to the user (or log them for debugging). For domain errors — verify and activate the sending domain in your dashboard. For scheduledAt — ensure the value is a valid ISO 8601 string representing a future time.
401 Unauthorized

Returned when the Authorization header is missing, malformed, or contains an invalid or deleted API key.

// HTTP 401
{
  "error": "Unauthorized"
}
How to handle: No detail is provided beyond the status code — this is intentional to avoid leaking information about key validity. Check that your key starts with qle_, is not deleted, and is being sent as Authorization: Bearer qle_…. See Authentication.
402 Payment Required

Monthly email limit reached. The response tells you exactly where you are against your plan's cap.

Single send

// HTTP 402
{
  "error": "Monthly limit reached",
  "limit": 3000,
  "used":  3000,
  "plan":  "Starter"
}

Batch — all-or-nothing pre-flight check

// HTTP 402
{
  "error": "This batch of 50 would exceed your monthly limit of 3000. You can send 12 more this month.",
  "canSend": 12
}
How to handle: For single sends — upgrade your plan or wait for the monthly reset (1st of the month, UTC). For batch — use canSend to split and send a smaller batch within your remaining allowance.
404 Not Found

The referenced resource does not exist or is not accessible from your account. On the public API surface, this is returned when the template field references a slug that does not match any saved template in your account.

// HTTP 404
{
  "error": "Template not found"
}
How to handle: Verify the template slug in your dashboard under Templates. Template slugs are case-sensitive. Templates belong to your account — you cannot reference another account's templates.
422 Unprocessable Entity

Returned when a template is found but fails to render. This happens when a {{variable}} in the template body has no matching key in the variables object — Handlebars strict mode is enabled, so every referenced variable must be provided.

// HTTP 422 — missing template variable
{
  "error": "Template render failed: \"firstName\" not defined"
}
How to handle: Pass all variables the template references in the variables object. Use {{#if var}}…{{/if}} in the template for optional values — references inside an #if block are not required at render time.
429 Too Many Requests

Returned for two independent limits: the per-minute request rate limit and the daily sending cap. The shapes are different — check the body to distinguish them.

Rate limit exceeded (100 requests / 60 seconds)

// HTTP 429
{
  "error": "Rate limit exceeded. Please slow down."
}

Rate limit headers are returned on every request — check X-RateLimit-Remaining to throttle proactively before you hit this. See Conventions → Rate limits.

Daily sending cap — single send

// HTTP 429
{
  "error": "Daily sending limit reached",
  "dailyLimit": 100,
  "dailySent":  100,
  "resetsAt":   "midnight UTC",
  "plan":       "Starter"
}

Daily sending cap — batch pre-flight

// HTTP 429
{
  "error": "This batch would exceed your daily limit of 100. You can send 20 more today.",
  "canSend": 20
}
How to handle: Rate limit — back off and retry after X-RateLimit-Reset. Daily cap — use canSend (batch) or wait until midnight UTC. Daily cap is floor(monthlyLimit / 30); upgrade your plan to increase it.
503 Service Unavailable

Account-level kill-switch. Quolle monitors bounce and complaint rates via CloudWatch. If either rate crosses a threshold, sending is automatically paused for the entire account to protect your domain reputation and inbox placement. This affects all sends regardless of template, recipient, or key used.

// HTTP 503 — bounce rate threshold exceeded
{
  "error": "Sending temporarily paused — account under review",
  "reason": "bounce"
}

// HTTP 503 — complaint rate threshold exceeded
{
  "error": "Sending temporarily paused — account under review",
  "reason": "complaint"
}
The pause does not lift automatically. A falling rate caused by sending stopping is not evidence that the underlying list problem is fixed. An operator must review and clear the pause manually. Contact support — do not retry in a loop.
How to handle: Stop retrying immediately (retries will all 503). Check your bounce/complaint rates in the dashboard. Contact support with your account details — include whether reason is "bounce" or "complaint" to speed up the review.
500 Server Error

An unexpected error occurred on our side. The body contains a message, but it may not always be actionable.

// HTTP 500
{
  "error": "Internal server error"
}
How to handle: Safe to retry with exponential backoff. If 500s persist, check the dashboard for any service notices, or contact support.