API Conventions

Behaviour that applies across every endpoint — stated once here so it is not repeated in each endpoint's docs.

Base URL

All API endpoints share a single base URL:

https://api.quolle.com

Every path in this documentation is relative to this base. There is no versioning prefix in the URL — breaking changes ship as new resources, not new versions.

Authentication

All public API requests require an API key in the Authorization header:

Authorization: Bearer qle_your_api_key

Keys start with qle_ and are created in the dashboard under API Keys. See the Authentication page for setup and security guidance.

Request format

All request bodies must be JSON. Set the content type on every POST request:

Content-Type: application/json

Requests without a body (GET, DELETE) do not need a Content-Type header.

Response format

All responses are JSON. The shape depends on whether the request succeeded.

Success

The response body is the resource or result directly — no wrapper envelope:

// POST /v1/emails/send — 200
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "message": "Email queued successfully"
}

Error

Errors always include a code and an error string. code is a stable identifier to branch on; error is a sentence for a person to read and may be reworded, so never match on it. Many errors carry additional fields alongside both:

// 402 — monthly limit reached
{
  "code":  "monthly_quota_exceeded",
  "error": "Monthly limit reached",
  "limit": 3000,
  "used":  3000,
  "plan":  "Starter"
}

See Errors → Error codes for every code and what to do about each one.

Validation errors (missing or wrong-typed fields) return a different shape — see Errors → 400 for the full Zod error structure.

Dates and times

All timestamps are ISO 8601 strings in UTC, ending in Z:

2026-07-05T12:00:00.000Z

When submitting a datetime (e.g. scheduledAt), use the same format. The API rejects non-ISO strings and datetimes that are not in the future.

Idempotency

POST /v1/emails/send, POST /v1/emails/batch, POST /v1/emails/receipt and POST /v1/broadcasts support an optional Idempotency-Key header. If you include one and the request succeeds, the response is cached for 24 hours under that key. A duplicate request with the same key, method and path returns the cached response immediately with an Idempotency-Replay: true response header — nothing is sent again.

Idempotency-Key: order_invoice_12345

Two cases answer 409 rather than replaying, and they ask for different things:

CodeMeansWhat to do
concurrent_idempotent_requestThe first request with this key is still running.Wait a few seconds and retry unchanged.
invalid_idempotent_requestThis key was already used with a different request body.Use a new key, or send the original request. The body is fingerprinted, so a key only ever answers for the request it was first used with.
Use a deterministic key tied to your business event (order ID, user ID + template slug, etc.) so retries are safe without needing to track whether the first request succeeded. A new key is needed if the request itself changes — correcting a typo and retrying under the old key is refused rather than silently ignored.

Where the unsubscribe link goes

Every marketing message carries one-click List-Unsubscribe headers, and a link is appended to the end of the body for you. You do not have to do anything.

You can place it yourself instead, with {{unsubscribeUrl}} anywhere in your HTML. Do that and the appended footer is left off — two unsubscribe links in one message reads as a mistake.

<a href="{{unsubscribeUrl}}">Unsubscribe</a>

{{preferencesUrl}} is also available, sending the reader to a page where they can turn off one topic rather than everything. It falls back to the unsubscribe link when the message has no topic.

Worth placing yourself on a long newsletter. Gmail clips a message past roughly 102KB behind "View entire message", and an appended footer is the first thing to disappear — a reader who cannot find a link marks the message as spam instead.

Pagination

List endpoints that return multiple records use a consistent pagination envelope:

{
  "data": [ /* array of records */ ],
  "pagination": {
    "total":  42,
    "page":   1,
    "limit":  20,
    "pages":  3
  }
}

Query parameters: page (default 1) and limit. pages is Math.ceil(total / limit).

Defaults differ by endpoint, because the records do:

EndpointDefaultMax
GET /v1/logs20100
GET /v1/contacts/:listId100500

total is always the true count, not the size of the page — a list of 40,000 contacts reports 40,000 whichever page you are on.

Retention and the clamped flag

List endpoints that support a date range (e.g. email logs) respect your plan's data-retention window. When your requested from date falls outside the retention window, it is silently moved forward to the earliest allowed date. The response includes a retention object that tells you when this happened:

"retention": {
  "days":    7,
  "clamped": true
}
FieldMeaning
daysHow many calendar days back your plan allows you to query
clampedtrue — your from date was earlier than the retention limit; results start from the retention boundary. false — your date range was within the allowed window

Upgrade your plan to increase the retention window.

Rate limits

All authenticated endpoints are limited to 100 requests per 60-second window per API key. Every response includes three headers:

HeaderValue
X-RateLimit-Limit100
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp (seconds) when the window resets

When the limit is exceeded, the API returns 429 with { "code": "rate_limit_exceeded", "error": "Rate limit exceeded. Please slow down." }. See Errors → 429 for the full shape and handling guidance.

Sending limits

In addition to the per-minute rate limit, your account has two independent sending caps that apply to POST /v1/emails/send and POST /v1/emails/batch:

LimitStatusResets
Monthly — total emails per calendar month4021st of each month (UTC)
Daily — floor(monthlyLimit / 30) per day429Midnight UTC

Unlimited plans (Enterprise) skip both checks. See Errors → 402 and Errors → 429 for the exact error shapes, including the batch variants.